From 0730d6ee2652054882a3fab859bc28387b30d0ac Mon Sep 17 00:00:00 2001 From: zzak Date: Wed, 18 Jan 2023 08:12:05 +0900 Subject: [PATCH] Run through AR Callbacks guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafael Mendonça França Co-authored-by: Lachlan Sylvester --- guides/source/active_record_callbacks.md | 186 ++++++++++++++------- guides/source/active_record_validations.md | 2 + 2 files changed, 129 insertions(+), 59 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 0afb01f82e..2ace4e6b44 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -3,12 +3,11 @@ Active Record Callbacks ======================= -This guide teaches you how to hook into the life cycle of your Active Record -objects. +This guide teaches you how to hook into the life cycle of your Active Record objects. After reading this guide, you will know: -* The life cycle of Active Record objects. +* When certain events occur during the life of an Active Record object * How to create callback methods that respond to events in the object life cycle. * How to create special classes that encapsulate common behavior for your callbacks. @@ -21,6 +20,19 @@ During the normal operation of a Rails application, objects may be created, upda Callbacks allow you to trigger logic before or after an alteration of an object's state. +```ruby +class Baby < ApplicationRecord + after_create -> { puts "Congratulations!" } +end +``` + +```irb +irb> @baby = Baby.create +Congratulations! +``` + +As you will see, there are many life cycle events and you can choose to hook into any of these either before, after, or even around them. + Callbacks Overview ------------------ @@ -57,7 +69,31 @@ class User < ApplicationRecord end ``` -Callbacks can also be registered to only fire on certain life cycle events: +Alternatively you can pass a proc to the callback to be triggered. + +```ruby +class User < ApplicationRecord + before_create ->(user) { user.name = user.login.capitalize if user.name.blank? } +end +``` + +Lastly, you can define your own custom callback object, which we will cover later in more detail [below](#callback-classes). + +```ruby +class User < ApplicationRecord + before_create MaybeAddName +end + +class MaybeAddName + def self.before_create(record) + if record.name.blank? + record.name = record.login.capitalize + end + end +end +``` + +Callbacks can also be registered to only fire on certain life cycle events, this allows complete control over when and in what context your callbacks are triggered. ```ruby class User < ApplicationRecord @@ -79,6 +115,8 @@ end It is considered good practice to declare callback methods as private. If left public, they can be called from outside of the model and violate the principle of object encapsulation. +WARNING. Avoid calls to `update`, `save` or other methods which create side-effects to the object inside your callback. For example, don't call `update(attribute: "value")` within a callback. This can alter the state of the model and may result in unexpected side effects during commit. Instead, you can safely assign values directly (for example, `self.attribute = "value"`) in `before_create` / `before_update` or earlier callbacks. + Available Callbacks ------------------- @@ -123,6 +161,7 @@ Here is a list with all the available Active Record callbacks, listed in the sam [`around_update`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-around_update [`before_update`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_update +WARNING. `after_save` runs both on create and update, but always _after_ the more specific callbacks `after_create` and `after_update`, no matter the order in which the macro calls were executed. ### Destroying an Object * [`before_destroy`][] @@ -134,21 +173,19 @@ Here is a list with all the available Active Record callbacks, listed in the sam [`around_destroy`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-around_destroy [`before_destroy`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_destroy -WARNING. `after_save` runs both on create and update, but always _after_ the more specific callbacks `after_create` and `after_update`, no matter the order in which the macro calls were executed. +NOTE: `before_destroy` callbacks should be placed before `dependent: :destroy` associations (or use the `prepend: true` option), to ensure they execute before the records are deleted by `dependent: :destroy`. More on [ordering callbacks](#order-callbacks) later. -WARNING. Avoid updating or saving attributes in callbacks. For example, don't call `update(attribute: "value")` within a callback. This can alter the state of the model and may result in unexpected side effects during commit. Instead, you can safely assign values directly (for example, `self.attribute = "value"`) in `before_create` / `before_update` or earlier callbacks. - -NOTE: `before_destroy` callbacks should be placed before `dependent: :destroy` -associations (or use the `prepend: true` option), to ensure they execute before -the records are deleted by `dependent: :destroy`. +WARNING. `after_commit` makes very different guarantees than `after_save`, `after_update`, and `after_destroy`. For example if an exception occurs in an `after_save` the transaction will be rolled back and the data will not be persisted. While anything that happens `after_commit` can guarantee the transaction has already completed and the data was persisted to the database. More on [transactional callbacks](#transaction-callbacks) below. ### `after_initialize` and `after_find` -The [`after_initialize`][] callback will be called whenever an Active Record object is instantiated, either by directly using `new` or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record `initialize` method. +Whenever an Active Record object is instantiated the [`after_initialize`][] callback will be called, either by directly using `new` or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record `initialize` method. -The [`after_find`][] callback will be called whenever Active Record loads a record from the database. `after_find` is called before `after_initialize` if both are defined. +When loading a record from the database the [`after_find`][] callback will be called. `after_find` is called before `after_initialize` if both are defined. -The `after_initialize` and `after_find` callbacks have no `before_*` counterparts, but they can be registered just like the other Active Record callbacks. +NOTE: The `after_initialize` and `after_find` callbacks have no `before_*` counterparts. + +They can be registered just like the other Active Record callbacks. ```ruby class User < ApplicationRecord @@ -200,31 +237,31 @@ You have touched an object It can be used along with `belongs_to`: ```ruby -class Employee < ApplicationRecord - belongs_to :company, touch: true +class Book < ApplicationRecord + belongs_to :library, touch: true after_touch do - puts 'An Employee was touched' + puts 'A Book was touched' end end -class Company < ApplicationRecord - has_many :employees - after_touch :log_when_employees_or_company_touched +class Library < ApplicationRecord + has_many :books + after_touch :log_when_books_or_library_touched private - def log_when_employees_or_company_touched - puts 'Employee/Company was touched' + def log_when_books_or_library_touched + puts 'Book/Library was touched' end end ``` ```irb -irb> @employee = Employee.last -=> # +irb> @book = Book.last +=> # -irb> @employee.touch # triggers @employee.company.touch -An Employee was touched -Employee/Company was touched +irb> @book.touch # triggers @book.library.touch +A Book was touched +Book/Library was touched => true ``` @@ -303,7 +340,7 @@ The whole callback chain is wrapped in a transaction. If any callback raises an throw :abort ``` -WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update` (which normally try to return `true` or `false`) to raise an exception. +WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Additionally, may break code that does not expect methods like `save` and `update` (which normally try to return `true` or `false`) to raise an exception. Relational Callbacks -------------------- @@ -337,11 +374,15 @@ Article destroyed Conditional Callbacks --------------------- -As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option. +As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a `Proc` or an `Array`. + +You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option. ### Using `:if` and `:unless` with a `Symbol` -You can associate the `:if` and `:unless` options with a symbol corresponding to the name of a predicate method that will get called right before the callback. When using the `:if` option, the callback won't be executed if the predicate method returns false; when using the `:unless` option, the callback won't be executed if the predicate method returns true. This is the most common option. Using this form of registration it is also possible to register several different predicates that should be called to check if the callback should be executed. +You can associate the `:if` and `:unless` options with a symbol corresponding to the name of a predicate method that will get called right before the callback. + +When using the `:if` option, the callback **won't** be executed if the predicate method returns **false**; when using the `:unless` option, the callback **won't** be executed if the predicate method returns **true**. This is the most common option. ```ruby class Order < ApplicationRecord @@ -349,6 +390,8 @@ class Order < ApplicationRecord end ``` +Using this form of registration it is also possible to register several different predicates that should be called to check if the callback should be executed. We will cover this [below](#multiple-callback-conditions). + ### Using `:if` and `:unless` with a `Proc` It is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners: @@ -368,6 +411,26 @@ class Order < ApplicationRecord end ``` +### Multiple Callback Conditions + +The `:if` and `:unless` options also accept an array of procs or method names as symbols: + +```ruby +class Comment < ApplicationRecord + before_save :filter_content, + if: [:subject_to_parental_control?, :untrusted_author?] +end +``` + +You can easily include a proc in the list of conditions: + +```ruby +class Comment < ApplicationRecord + before_save :filter_content, + if: [:subject_to_parental_control?, Proc.new { untrusted_author? }] +end +``` + ### Using both :if and :unless Callbacks can mix both `:if` and `:unless` in the same declaration: @@ -380,17 +443,6 @@ class Comment < ApplicationRecord end ``` -### Multiple Callback Conditions - -The `:if` and `:unless` options also accept an array of procs or method names as symbols: - -```ruby -class Comment < ApplicationRecord - before_save :filter_content, - if: [:subject_to_parental_control?, :untrusted_author?] -end -``` - The callback only runs when all the `:if` conditions and none of the `:unless` conditions are evaluated to `true`. Callback Classes @@ -398,43 +450,43 @@ Callback Classes Sometimes the callback methods that you'll write will be useful enough to be reused by other models. Active Record makes it possible to create classes that encapsulate the callback methods, so they can be reused. -Here's an example where we create a class with an `after_destroy` callback for a `PictureFile` model: +Here's an example where we create a class with an `after_destroy` callback to deal with the clean up of discarded files on the filesystem. This behavior may not be unique to our `PictureFile` model and we may want to share it, so it's a good idea to encapsulate this into a separate class. This will make testing that behavior and changing it much easier. ```ruby -class PictureFileCallbacks - def after_destroy(picture_file) - if File.exist?(picture_file.filepath) - File.delete(picture_file.filepath) +class FileDestroyerCallback + def after_destroy(file) + if File.exist?(file.filepath) + File.delete(file.filepath) end end end ``` -When declared inside a class, as above, the callback methods will receive the model object as a parameter. We can now use the callback class in the model: +When declared inside a class, as above, the callback methods will receive the model object as a parameter. This will work on any model that uses the class like so: ```ruby class PictureFile < ApplicationRecord - after_destroy PictureFileCallbacks.new + after_destroy FileDestroyerCallback.new end ``` -Note that we needed to instantiate a new `PictureFileCallbacks` object, since we declared our callback as an instance method. This is particularly useful if the callbacks make use of the state of the instantiated object. Often, however, it will make more sense to declare the callbacks as class methods: +Note that we needed to instantiate a new `FileDestroyerCallback` object, since we declared our callback as an instance method. This is particularly useful if the callbacks make use of the state of the instantiated object. Often, however, it will make more sense to declare the callbacks as class methods: ```ruby -class PictureFileCallbacks - def self.after_destroy(picture_file) - if File.exist?(picture_file.filepath) - File.delete(picture_file.filepath) +class FileDestroyerCallback + def self.after_destroy(file) + if File.exist?(file.filepath) + File.delete(file.filepath) end end end ``` -If the callback method is declared this way, it won't be necessary to instantiate a `PictureFileCallbacks` object. +When the callback method is declared this way, it won't be necessary to instantiate a new `FileDestroyerCallback` object in our model. ```ruby class PictureFile < ApplicationRecord - after_destroy PictureFileCallbacks + after_destroy FileDestroyerCallback end ``` @@ -443,6 +495,8 @@ You can declare as many callbacks as you want inside your callback classes. Transaction Callbacks --------------------- +### Dealing with consistency + There are two additional callbacks that are triggered by the completion of a database transaction: [`after_commit`][] and [`after_rollback`][]. These callbacks are very similar to the `after_save` callback except that they don't execute until after database changes have either been committed or rolled back. They are most useful when your active record models need to interact with external systems which are not part of the database transaction. Consider, for example, the previous example where the `PictureFile` model needs to delete a file after the corresponding record is destroyed. If anything raises an exception after the `after_destroy` callback is called and the transaction rolls back, the file will have been deleted and the model will be left in an inconsistent state. For example, suppose that `picture_file_2` in the code below is not valid and the `save!` method raises an error. @@ -468,8 +522,9 @@ class PictureFile < ApplicationRecord end ``` -NOTE: The `:on` option specifies when a callback will be fired. If you -don't supply the `:on` option the callback will fire for every action. +NOTE: The `:on` option specifies when a callback will be fired. If you don't supply the `:on` option the callback will fire for every action. + +### Context matters Since using the `after_commit` callback only on create, update, or delete is common, there are aliases for those operations: @@ -515,9 +570,9 @@ irb> @user.save # updating @user User was saved to database ``` -There is also an alias for using the `after_commit` callback for both create and update together: +### `after_save_commit` -* [`after_save_commit`][] +There is also [`after_save_commit`][], which is an alias for using the `after_commit` callback for both create and update together: ```ruby class User < ApplicationRecord @@ -538,7 +593,20 @@ irb> @user.save # updating @user User was saved to database ``` +### Transactional callback ordering + +When defining multiple transactional `after_` callbacks (`after_commit`, `after_rollback`, etc), the order will be reversed from when they are defined. + +```ruby +class User < ActiveRecord::Base + after_commit { puts("this actually gets called second") } + after_commit { puts("this actually gets called first") } +end +``` + +NOTE: This applies to all `after_*_commit` variations too, such as `after_destroy_commit`. + [`after_create_commit`]: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_create_commit [`after_destroy_commit`]: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_destroy_commit [`after_save_commit`]: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_save_commit -[`after_update_commit`]: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_update_commit +[`after_update_commit`]: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_update_commit \ No newline at end of file diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index f0d9a69a5d..a119ac6c93 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -1108,6 +1108,8 @@ irb> person.errors.messages => {:email=>["has already been taken"], :age=>["is not a number"], :name=>["can't be blank"]} ``` +We will cover more use-cases for `on:` in the [callbacks guide](active_record_callbacks.html). + Strict Validations ------------------