Run through AR Callbacks guide

Co-authored-by: Rafael Mendonça França <rafael@franca.dev>
Co-authored-by: Lachlan Sylvester <lachlan.sylvester@3plearning.com>
This commit is contained in:
zzak 2023-01-18 08:12:05 +09:00
parent da73378244
commit 0730d6ee26
2 changed files with 129 additions and 59 deletions

@ -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
=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
irb> @book = Book.last
=> #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
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

@ -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
------------------