From 82eff47343269c77bd8c567f0fe274d95b7c3777 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 28 Mar 2024 13:16:11 +0200 Subject: [PATCH 01/86] base changes from the old PR --- guides/source/active_record_callbacks.md | 655 +++++++++++++++++------ 1 file changed, 496 insertions(+), 159 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index a12e4d554863b..f0f7cd6d05284 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -3,22 +3,31 @@ 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: -* 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. +* 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. -------------------------------------------------------------------------------- The Object Life Cycle --------------------- -During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this *object life cycle* so that you can control your application and its data. +During the normal operation of a Rails application, objects may be created, +updated, and destroyed. Active Record provides hooks into this *object life +cycle* so that you can control your application and its data. -Callbacks allow you to trigger logic before or after an alteration of an object's state. +Callbacks allow you to trigger logic before or after an alteration of an +object's state. They are methods that get called at certain moments of an +object's life cycle. With callbacks it is possible to write code that will run +whenever an Active Record object is created, saved, updated, deleted, validated, +or loaded from the database. ```ruby class Baby < ApplicationRecord @@ -31,80 +40,95 @@ 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. +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 +Callback Registration ------------------ -Callbacks are methods that get called at certain moments of an object's life cycle. With callbacks it is possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database. +To use the available callbacks, you need to implement and register them. +Implementation can be done in a multitude of ways like using ordinary methods, +blocks and procs, or defining custom callback objects using callback classes. +Let's go through each of these implementation techniques. -### Callback Registration - -In order to use the available callbacks, you need to register them. You can implement the callbacks as ordinary methods and use a macro-style class method to register them as callbacks: +You can implement the callbacks as a **macro-style method that calls an ordinary +method** for registration. ```ruby class User < ApplicationRecord - validates :login, :email, presence: true + validates :username, :email, presence: true - before_validation :ensure_login_has_a_value + before_validation :ensure_username_has_value private - def ensure_login_has_a_value - if login.blank? - self.login = email unless email.blank? + def ensure_username_has_value + if username.blank? + self.username = email end end end ``` -The macro-style class methods can also receive a block. Consider using this style if the code inside your block is so short that it fits in a single line: +The **macro-style class methods can also receive a block**. Consider using this +style if the code inside your block is so short that it fits in a single line: ```ruby class User < ApplicationRecord - validates :login, :email, presence: true + validates :username, :email, presence: true - before_create do - self.name = login.capitalize if name.blank? + before_validation do + self.username = email if username.blank? end end ``` -Alternatively you can pass a proc to the callback to be triggered. +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? } + validates :username, :email, presence: true + + before_validation ->(user) { user.username = user.email if user.username.blank? } end ``` -Lastly, you can define your own custom callback object, which we will cover later in more detail [below](#callback-classes). +Lastly, you can define **a custom callback object**, which we will cover later +in more detail [below](#callback-classes). ```ruby class User < ApplicationRecord - before_create MaybeAddName + validates :username, :email, presence: true + + before_validation AddUsername end -class MaybeAddName - def self.before_create(record) - if record.name.blank? - record.name = record.login.capitalize +class AddUsername + def self.before_validation(record) + if record.username.blank? + record.username = record.email 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. +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 - before_validation :normalize_name, on: :create + validates :username, :email, presence: true + + before_validation :ensure_username_has_value, on: :create # :on takes an array as well after_validation :set_location, on: [ :create, :update ] private - def normalize_name - self.name = name.downcase.titleize + def ensure_username_has_value + if username.blank? + self.username = email + end end def set_location @@ -113,14 +137,24 @@ class User < ApplicationRecord 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. +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. Refrain from using methods like `update`, `save`, or any other methods +that cause side effects on the object within your callback functions. For +instance, avoid calling `update(attribute: "value")` inside a callback. This +practice can modify the model's state and potentially lead to unforeseen side +effects during commit. Instead, you can assign values directly (e.g., +`self.attribute = "value"`) in `before_create`, `before_update`, or earlier +callbacks for a safer approach. -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 ------------------- -Here is a list with all the available Active Record callbacks, listed in the same order in which they will get called during the respective operations: +Here is a list with all the available Active Record callbacks, listed **in the +order in which they will get called** during the respective operations: ### Creating an Object @@ -134,16 +168,145 @@ Here is a list with all the available Active Record callbacks, listed in the sam * [`after_save`][] * [`after_commit`][] / [`after_rollback`][] -[`after_create`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_create -[`after_commit`]: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_commit -[`after_rollback`]: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_rollback -[`after_save`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_save -[`after_validation`]: https://api.rubyonrails.org/classes/ActiveModel/Validations/Callbacks/ClassMethods.html#method-i-after_validation -[`around_create`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-around_create -[`around_save`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-around_save -[`before_create`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_create -[`before_save`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_save -[`before_validation`]: https://api.rubyonrails.org/classes/ActiveModel/Validations/Callbacks/ClassMethods.html#method-i-before_validation +[`after_create`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_create +[`after_commit`]: + https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_commit +[`after_rollback`]: + https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_rollback +[`after_save`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_save +[`after_validation`]: + https://api.rubyonrails.org/classes/ActiveModel/Validations/Callbacks/ClassMethods.html#method-i-after_validation +[`around_create`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-around_create +[`around_save`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-around_save +[`before_create`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_create +[`before_save`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_save +[`before_validation`]: + https://api.rubyonrails.org/classes/ActiveModel/Validations/Callbacks/ClassMethods.html#method-i-before_validation + +There are examples below that show how to use these callbacks. We've grouped +them by the operation they are associated with, however they can be used in any +combination. + +#### Validation Callbacks + +Validation callbacks are triggered by the `valid?` method. They can be called +before and after the validation phase. + +```ruby +class User < ApplicationRecord + validates :name, presence: true + before_validation :normalize_name + after_validation :check_errors + + private + def normalize_name + self.name = name.downcase.titleize if name.present? + Rails.logger.info("Name normalized to #{name}") + end + + def check_errors + if errors.any? + Rails.logger.error("Validation failed: #{errors.full_messages.join(', ')}") + end + end +end +``` + +```irb +irb> user = User.new(name: "", email: "john.doe@example.com", password: "abc123456") +=> # + +irb> user.valid? +Name normalized to +Validation failed: Name can't be blank +=> false +``` + +#### Save Callbacks + +Save callbacks are triggered by the `save` method. They can be called before, +after and around the object is saved. + +```ruby +class User < ApplicationRecord + before_save :encrypt_password + around_save :log_saving + after_save :update_cache + + private + def encrypt_password + self.password = BCrypt::Password.create(password) + Rails.logger.info("Password encrypted for user with email: #{email}") + end + + def log_saving + Rails.logger.info("Saving user with email: #{email}") + yield + Rails.logger.info("User saved with email: #{email}") + end + + def update_cache + Rails.cache.write("user_data", attributes) + Rails.logger.info("Update Cache") + end +end +``` + +```irb +irb> user = User.create(name: "Jane Doe", email: "jane.doe@example.com") + +Password encrypted for user with email: jane.doe@example.com +Saving user with email: jane.doe@example.com +User saved with email: jane.doe@example.com +Update Cache +=> # +``` + +#### Create Callbacks + +Create callbacks are triggered by the `create` method. They can be called +before, after and around the object is created. + +```ruby +class User < ApplicationRecord + before_create :set_default_role + around_create :log_creation + after_create :send_welcome_email + + private + def set_default_role + self.role = "user" + Rails.logger.info("User role set to default: user") + end + + def log_creation + Rails.logger.info("Creating user with email: #{email}") + yield + Rails.logger.info("User created with email: #{email}") + end + + def send_welcome_email + UserMailer.welcome_email(self).deliver_later + Rails.logger.info("User welcome email sent to: #{email}") + end +end +``` + +```irb +irb> user = User.create(name: "John Doe", email: "john.doe@example.com") + +User role set to default: user +Creating user with email: john.doe@example.com +User created with email: john.doe@example.com +User welcome email sent to: john.doe@example.com +=> # +``` ### Updating an Object @@ -157,11 +320,61 @@ Here is a list with all the available Active Record callbacks, listed in the sam * [`after_save`][] * [`after_commit`][] / [`after_rollback`][] -[`after_update`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_update -[`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 +[`after_update`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_update +[`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. The `after_save` callback is triggered on both a create and update. +However, it consistently executes after the more specific callbacks +`after_create` and `after_update`, regardless of the sequence in which the macro +calls were made. + +We've already covered [validation](#validation-callbacks) and +[save](#save-callbacks) callbacks. `after_commit` / `after_rollback` examples +can be found [here](#after-commit-and-after-rollback). -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. +#### Update Callbacks + +```ruby +class User < ApplicationRecord + before_update :check_role_change + around_update :log_updating + after_update :send_update_email + + private + def check_role_change + if role_changed? + Rails.logger.info("User role changed to #{role}") + end + end + + def log_updating + Rails.logger.info("Updating user with email: #{email}") + yield + Rails.logger.info("User updated with email: #{email}") + end + + def send_update_email + UserMailer.update_email(self).deliver_later + Rails.logger.info("Update email sent to: #{email}") + end +end +``` + +```irb +irb> user = User.find(1) +=> # + +irb> user.update(role: "admin") +User role changed to admin +Updating user with email: john.doe@example.com +User updated with email: john.doe@example.com +Update email sent to: john.doe@example.com +``` ### Destroying an Object @@ -170,21 +383,76 @@ WARNING. `after_save` runs both on create and update, but always _after_ the mor * [`after_destroy`][] * [`after_commit`][] / [`after_rollback`][] -[`after_destroy`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_destroy -[`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 +[`after_destroy`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_destroy +[`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 + +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. However, anything that happens `after_commit` can guarantee the +transaction has already been completed and the data was persisted to the +database. More on [transactional callbacks](#transaction-callbacks) below. + +```ruby +class User < ApplicationRecord + before_destroy :check_admin_count + around_destroy :log_destroy_operation + after_destroy :notify_users + + private + def check_admin_count + if User.where(admin: true).count == 1 && admin? + raise StandardError.new("Cannot delete the last admin user") + end + Rails.logger.info("Checked the admin count") + end + + def log_destroy_operation + Rails.logger.info("About to destroy user with ID #{id}") + yield + Rails.logger.info("User with ID #{id} destroyed successfully") + end + + def notify_users + Rails.logger.info("Notification sent to other users about user deletion") + end +end +``` + +```irb +irb> user = User.find(1) +=> # -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`. +irb> user.destroy +Checked the admin count +About to destroy user with ID 1 +User with ID 1 destroyed successfully +Notification sent to other users about user deletion +``` -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_commit` / `after_rollback` examples can be found +[here](active_record_callbacks.html#after-commit-and-after-rollback). ### `after_initialize` and `after_find` -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. +Whenever an Active Record object is instantiated, either by directly using `new` +or when a record is loaded from the database, then the [`after_initialize`][] +callback will be called. It can be useful to avoid the need to directly override +your Active Record `initialize` method. -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. +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. -NOTE: The `after_initialize` and `after_find` callbacks have no `before_*` counterparts. +NOTE: The `after_initialize` and `after_find` callbacks have no `before_*` +counterparts. They can be registered just like the other Active Record callbacks. @@ -211,12 +479,15 @@ You have initialized an object! => # ``` -[`after_find`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_find -[`after_initialize`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_initialize +[`after_find`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_find +[`after_initialize`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_initialize ### `after_touch` -The [`after_touch`][] callback will be called whenever an Active Record object is touched. +The [`after_touch`][] callback will be called whenever an Active Record object +is touched. ```ruby class User < ApplicationRecord @@ -266,7 +537,8 @@ Book/Library was touched => true ``` -[`after_touch`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_touch +[`after_touch`]: + https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_touch Running Callbacks ----------------- @@ -289,86 +561,92 @@ The following methods trigger callbacks: * `update` * `update!` * `valid?` +* `validate` -Additionally, the `after_find` callback is triggered by the following finder methods: +Additionally, the `after_find` callback is triggered by the following finder +methods: * `all` * `first` * `find` * `find_by` +* `find_by!` * `find_by_*` * `find_by_*!` * `find_by_sql` * `last` +* `sole` +* `take` -The `after_initialize` callback is triggered every time a new object of the class is initialized. +The `after_initialize` callback is triggered every time a new object of the +class is initialized. -NOTE: The `find_by_*` and `find_by_*!` methods are dynamic finders generated automatically for every attribute. Learn more about them at the [Dynamic finders section](active_record_querying.html#dynamic-finders) +NOTE: The `find_by_*` and `find_by_*!` methods are dynamic finders generated +automatically for every attribute. Learn more about them in the [Dynamic finders +section](active_record_querying.html#dynamic-finders). Skipping Callbacks ------------------ -Just as with validations, it is also possible to skip callbacks by using the following methods: - -* [`decrement!`][] -* [`decrement_counter`][] -* [`delete`][] -* [`delete_all`][] -* [`delete_by`][] -* [`increment!`][] -* [`increment_counter`][] -* [`insert`][] -* [`insert!`][] -* [`insert_all`][] -* [`insert_all!`][] -* [`touch_all`][] -* [`update_column`][] -* [`update_columns`][] -* [`update_all`][] -* [`update_counters`][] -* [`upsert`][] -* [`upsert_all`][] - -These methods should be used with caution, however, because important business rules and application logic may be kept in callbacks. Bypassing them without understanding the potential implications may lead to invalid data. Refer to the method documentation to learn more. - -[`decrement!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-decrement-21 -[`decrement_counter`]: https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-decrement_counter -[`delete`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-delete -[`delete_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_all -[`delete_by`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_by -[`increment!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-increment-21 -[`increment_counter`]: https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-increment_counter -[`insert`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert -[`insert!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert-21 -[`insert_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all -[`insert_all!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all-21 -[`touch_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-touch_all -[`update_column`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column -[`update_columns`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_columns -[`update_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_all -[`update_counters`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_counters -[`upsert`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert -[`upsert_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all +Just as with [validations](active_record_validations.html), it is also possible +to skip callbacks by using the following methods: + +* `decrement!` +* `decrement_counter` +* `delete` +* `delete_all` +* `delete_by` +* `increment!` +* `increment_counter` +* `insert` +* `insert!` +* `insert_all` +* `insert_all!` +* `touch_all` +* `update_column` +* `update_columns` +* `update_all` +* `update_counters` +* `upsert` +* `upsert_all` + +WARNING. These methods should be used with caution because there may be +important business rules and application logic in callbacks that you do not want +to bypass. Bypassing them without understanding the potential implications may +lead to invalid data. Halting Execution ----------------- -As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed. +As you start registering new callbacks for your models, they will be queued for +execution. This queue will include all of your model's validations, the +registered callbacks, and the database operation to be executed. -The whole callback chain is wrapped in a transaction. If any callback raises an exception, the execution chain gets halted and a ROLLBACK is issued. To intentionally stop a chain use: +The whole callback chain is wrapped in a transaction. If any callback raises an +exception, the execution chain gets halted and a ROLLBACK is issued. To +intentionally halt a chain use: ```ruby 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. 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. +WARNING. Any exception that is not `ActiveRecord::Rollback` or +`ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback +chain is halted. Additionally, it may break code that does not expect methods +like `save` and `update` (which normally try to return `true` or `false`) to +raise an exception. -NOTE: If an `ActiveRecord::RecordNotDestroyed` is raised within `after_destroy`, `before_destroy` or `around_destroy` callback, it will not be re-raised and the `destroy` method will return `false`. +NOTE: If an `ActiveRecord::RecordNotDestroyed` is raised within `after_destroy`, +`before_destroy` or `around_destroy` callback, it will not be re-raised and the +`destroy` method will return `false`. Relational Callbacks -------------------- -Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Article` model: +Callbacks work through model relationships, and can even be defined by them. +Suppose an example where a user has many articles. A user's articles should be +destroyed if the user is destroyed. Let's add an `after_destroy` callback to the +`User` model by way of its relationship to the `Article` model: ```ruby class User < ApplicationRecord @@ -397,14 +675,17 @@ Article destroyed Association Callbacks --------------------- -Association callbacks are similar to normal callbacks, but they are triggered by events in the life cycle of a collection. There are four available association callbacks: +Association callbacks are similar to normal callbacks, but they are triggered by +events in the life cycle of the associated collection. There are four available +association callbacks: * `before_add` * `after_add` * `before_remove` * `after_remove` -You define association callbacks by adding options to the association declaration. For example: +You define association callbacks by adding options to the association +declaration. For example: ```ruby class Author < ApplicationRecord @@ -435,9 +716,9 @@ class Author < ApplicationRecord end ``` -If a `before_add` callback throws `:abort`, the object does not get added to -the collection. Similarly, if a `before_remove` callback throws `:abort`, the -object does not get removed from the collection: +If a `before_add` callback throws `:abort`, the object does not get added to the +collection. Similarly, if a `before_remove` callback throws `:abort`, the object +does not get removed from the collection: ```ruby # book won't be added if the limit has been reached @@ -446,7 +727,8 @@ def check_credit_limit(book) end ``` -NOTE: These callbacks are called only when the associated objects are added or removed through the association collection: +NOTE: These callbacks are called only when the associated objects are added or +removed through the association collection: ```ruby # Triggers `before_add` callback @@ -460,15 +742,24 @@ book.update(author_id: 1) 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`. +As with [validations](active_record_validations.html), 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. +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. +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. +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 @@ -476,30 +767,35 @@ 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 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: +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: ```ruby class Order < ApplicationRecord before_save :normalize_card_number, - if: Proc.new { |order| order.paid_with_card? } + if: proc { |order| order.paid_with_card? } end ``` -As the proc is evaluated in the context of the object, it is also possible to write this as: +As the proc is evaluated in the context of the object, it is also possible to +write this as: ```ruby class Order < ApplicationRecord - before_save :normalize_card_number, if: Proc.new { paid_with_card? } + before_save :normalize_card_number, if: proc { paid_with_card? } end ``` ### Multiple Callback Conditions -The `:if` and `:unless` options also accept an array of procs or method names as symbols: +The `:if` and `:unless` options also accept an array of procs or method names as +symbols: ```ruby class Comment < ApplicationRecord @@ -513,7 +809,7 @@ 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? }] + if: [:subject_to_parental_control?, proc { untrusted_author? }] end ``` @@ -524,19 +820,26 @@ Callbacks can mix both `:if` and `:unless` in the same declaration: ```ruby class Comment < ApplicationRecord before_save :filter_content, - if: Proc.new { forum.parental_control? }, - unless: Proc.new { author.trusted? } + if: proc { forum.parental_control? }, + unless: proc { author.trusted? } end ``` -The callback only runs when all the `:if` conditions and none of the `:unless` conditions are evaluated to `true`. +The callback only runs when all the `:if` conditions and none of the `:unless` +conditions are evaluated to `true`. 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. +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 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. +Here's an example where we create a class with an `after_destroy` callback to +deal with the cleanup 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 FileDestroyerCallback @@ -548,7 +851,9 @@ class FileDestroyerCallback end ``` -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: +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 @@ -556,7 +861,10 @@ class PictureFile < ApplicationRecord end ``` -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: +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 FileDestroyerCallback @@ -568,7 +876,8 @@ class FileDestroyerCallback end ``` -When the callback method is declared this way, it won't be necessary to instantiate a new `FileDestroyerCallback` object in our model. +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 @@ -583,9 +892,19 @@ Transaction Callbacks ### `after_commit` and `after_rollback` -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. +Two additional callbacks 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 +that 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. +Consider if the `PictureFile` model, from the previous example, 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, then 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. ```ruby PictureFile.transaction do @@ -608,23 +927,31 @@ 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. -WARNING. When a transaction completes, the `after_commit` or `after_rollback` callbacks are called for all models created, updated, or destroyed within that transaction. However, if an exception is raised within one of these callbacks, the exception will bubble up and any remaining `after_commit` or `after_rollback` methods will _not_ be executed. As such, if your callback code could raise an exception, you'll need to rescue it and handle it within the callback in order to allow other callbacks to run. +WARNING. When a transaction completes, the `after_commit` or `after_rollback` +callbacks are called for all models created, updated, or destroyed within that +transaction. However, if an exception is raised within one of these callbacks, +the exception will bubble up and any remaining `after_commit` or +`after_rollback` methods will _not_ be executed. As such, if your callback code +could raise an exception, you'll need to rescue it and handle it within the +callback in order to allow other callbacks to run. -WARNING. The code executed within `after_commit` or `after_rollback` callbacks is itself not enclosed within a transaction. +WARNING. The code executed within `after_commit` or `after_rollback` callbacks +is itself not enclosed within a transaction. WARNING. In the context of a single transaction, if you interact with multiple loaded objects that represent the same record in the database, there's a crucial behavior in the `after_commit` and `after_rollback` callbacks to note. These callbacks are triggered only for the first object of the specific record that -undergoes a change within the transaction. Other loaded objects, despite -representing the same database record, will not have their respective -`after_commit` or `after_rollback` callbacks triggered. This nuanced behavior is -particularly impactful in scenarios where you expect independent callback -execution for each object associated with the same database record. It can -influence the flow and predictability of callback sequences, leading to potential -inconsistencies in application logic following the transaction. +changes within the transaction. Other loaded objects, despite representing the +same database record, will not have their respective `after_commit` or +`after_rollback` callbacks triggered. This nuanced behavior is particularly +impactful in scenarios where you expect independent callback execution for each +object associated with the same database record. It can influence the flow and +predictability of callback sequences, leading to potential inconsistencies in +application logic following the transaction. ### Aliases for `after_commit` @@ -647,7 +974,10 @@ class PictureFile < ApplicationRecord end ``` -WARNING. Using both `after_create_commit` and `after_update_commit` with the same method name will only allow the last callback defined to take effect, as they both internally alias to `after_commit` which overrides previously defined callbacks with the same method name. +WARNING. Using both `after_create_commit` and `after_update_commit` with the +same method name will only allow the last callback defined to take effect, as +they both internally alias to `after_commit` which overrides previously defined +callbacks with the same method name. ```ruby class User < ApplicationRecord @@ -670,7 +1000,8 @@ User was saved to database ### `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: +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 @@ -695,7 +1026,7 @@ User was saved to database By default, callbacks will run in the order they are defined. However, when defining multiple transactional `after_` callbacks (`after_commit`, -`after_rollback`, etc), the order could be reversed from when they are defined. +`after_rollback`, etc), the order can be reversed from when they are defined. ```ruby class User < ActiveRecord::Base @@ -704,7 +1035,8 @@ class User < ActiveRecord::Base end ``` -NOTE: This applies to all `after_*_commit` variations too, such as `after_destroy_commit`. +NOTE: This applies to all `after_*_commit` variations too, such as +`after_destroy_commit`. This order can be set via configuration: @@ -712,10 +1044,15 @@ This order can be set via configuration: config.active_record.run_after_transaction_callbacks_in_order_defined = false ``` -When set to `true` (the default from Rails 7.1), callbacks are executed in the order they -are defined. When set to `false`, the order is reversed, just like in the example above. - -[`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 +When set to `true` (the default from Rails 7.1), callbacks are executed in the +order they are defined. When set to `false`, the order is reversed, just like in +the example above. + +[`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 From 1b57299a5d9db1d60291bc1cc900ada403a84f4d Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 28 Mar 2024 13:22:22 +0200 Subject: [PATCH 02/86] rename callback classes to callback objects --- guides/source/active_record_callbacks.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index f0f7cd6d05284..b1af48ac0d17a 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -48,7 +48,7 @@ Callback Registration To use the available callbacks, you need to implement and register them. Implementation can be done in a multitude of ways like using ordinary methods, -blocks and procs, or defining custom callback objects using callback classes. +blocks and procs, or defining custom callback objects using classes. Let's go through each of these implementation techniques. You can implement the callbacks as a **macro-style method that calls an ordinary @@ -92,8 +92,7 @@ class User < ApplicationRecord end ``` -Lastly, you can define **a custom callback object**, which we will cover later -in more detail [below](#callback-classes). +Lastly, you can define **a custom callback object**, as shown below. We will cover these [later in more detail](#callback-objects). ```ruby class User < ApplicationRecord @@ -828,7 +827,7 @@ end The callback only runs when all the `:if` conditions and none of the `:unless` conditions are evaluated to `true`. -Callback Classes +Callback Objects ---------------- Sometimes the callback methods that you'll write will be useful enough to be @@ -885,7 +884,7 @@ class PictureFile < ApplicationRecord end ``` -You can declare as many callbacks as you want inside your callback classes. +You can declare as many callbacks as you want inside your callback objects. Transaction Callbacks --------------------- From 7e9396528cdb754247ea8cb830c1d74b2ce54022 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 28 Mar 2024 13:24:35 +0200 Subject: [PATCH 03/86] remove instance var --- guides/source/active_record_callbacks.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index b1af48ac0d17a..549f9b2e0ede2 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -36,7 +36,7 @@ end ``` ```irb -irb> @baby = Baby.create +irb> baby = Baby.create Congratulations! ``` @@ -229,8 +229,9 @@ Validation failed: Name can't be blank #### Save Callbacks -Save callbacks are triggered by the `save` method. They can be called before, -after and around the object is saved. +Save callbacks are triggered whenever the record is persisted (i.e. "saved") to +the underlying database, via the `create`, `update`, or `save` methods. They are +called before, after and around the object is saved. ```ruby class User < ApplicationRecord From 3afdb97191a2aef35fc5dbb5cbb9878a09e5f66e Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 28 Mar 2024 13:25:50 +0200 Subject: [PATCH 04/86] oxford comma --- guides/source/active_record_callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 549f9b2e0ede2..6118f34062559 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -231,7 +231,7 @@ Validation failed: Name can't be blank Save callbacks are triggered whenever the record is persisted (i.e. "saved") to the underlying database, via the `create`, `update`, or `save` methods. They are -called before, after and around the object is saved. +called before, after, and around the object is saved. ```ruby class User < ApplicationRecord @@ -271,7 +271,7 @@ Update Cache #### Create Callbacks Create callbacks are triggered by the `create` method. They can be called -before, after and around the object is created. +before, after, and around the object is created. ```ruby class User < ApplicationRecord From 924f9913a3038b946f6585699c9dd863f2f64505 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 28 Mar 2024 13:35:05 +0200 Subject: [PATCH 05/86] updates as per suggestions --- guides/source/active_record_callbacks.md | 31 +++++++++++++----------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 6118f34062559..8c150a18ed352 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -270,8 +270,10 @@ Update Cache #### Create Callbacks -Create callbacks are triggered by the `create` method. They can be called -before, after, and around the object is created. +Create callbacks are triggered whenever the record is persisted (i.e. "saved") +to the underlying database **for the first time**, in other words, when we're +saving a new record, via the `create`, `save`, or `update` methods. They are +called before, after and around the object is created. ```ruby class User < ApplicationRecord @@ -498,10 +500,10 @@ end ``` ```irb -irb> u = User.create(name: 'Kuldeep') +irb> user = User.create(name: 'Kuldeep') => # -irb> u.touch +irb> user.touch You have touched an object => true ``` @@ -528,10 +530,10 @@ end ``` ```irb -irb> @book = Book.last +irb> book = Book.last => # -irb> @book.touch # triggers @book.library.touch +irb> book.touch # triggers book.library.touch A Book was touched Book/Library was touched => true @@ -685,15 +687,16 @@ association callbacks: * `after_remove` You define association callbacks by adding options to the association -declaration. For example: +declaration. For example ```ruby class Author < ApplicationRecord has_many :books, before_add: :check_credit_limit - def check_credit_limit(book) - # ... - end + private + def check_credit_limit(book) + # ... + end end ``` @@ -992,9 +995,9 @@ end ``` ```irb -irb> @user = User.create # prints nothing +irb> user = User.create # prints nothing -irb> @user.save # updating @user +irb> user.save # updating @user User was saved to database ``` @@ -1015,10 +1018,10 @@ end ``` ```irb -irb> @user = User.create # creating a User +irb> user = User.create # creating a User User was saved to database -irb> @user.save # updating @user +irb> user.save # updating user User was saved to database ``` From b1861ff55410c0d0f1c903db9fd3497ccf3efaa6 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 28 Mar 2024 17:02:52 +0200 Subject: [PATCH 06/86] explain the use cases of callbacks --- guides/source/active_record_callbacks.md | 27 +++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 8c150a18ed352..05a2c5bc3d453 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -646,6 +646,7 @@ Relational Callbacks -------------------- Callbacks work through model relationships, and can even be defined by them. + Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Article` model: @@ -686,15 +687,19 @@ association callbacks: * `before_remove` * `after_remove` -You define association callbacks by adding options to the association -declaration. For example +You can define association callbacks by adding options to the association. + +Suppose you have an example where an author can have many books. However, before +adding a book to the authors collection, you want to ensure that the author has +not reached their book limit. You can do this by adding a `before_add` callback +to the `Author` model for the `has_many: books` association. ```ruby class Author < ApplicationRecord - has_many :books, before_add: :check_credit_limit + has_many :books, before_add: :check_limit private - def check_credit_limit(book) + def check_limit(book) # ... end end @@ -702,14 +707,16 @@ end Rails passes the object being added or removed to the callback. -You can stack callbacks on a single event by passing them as an array: +At times you may want to perform multiple actions on the associated object. In +this case, you can stack callbacks on a single event by passing them as an +array: ```ruby class Author < ApplicationRecord has_many :books, - before_add: [:check_credit_limit, :calculate_shipping_charges] + before_add: [:check_limit, :calculate_shipping_charges] - def check_credit_limit(book) + def check_limit(book) # ... end @@ -725,13 +732,13 @@ does not get removed from the collection: ```ruby # book won't be added if the limit has been reached -def check_credit_limit(book) +def check_limit(book) throw(:abort) if limit_reached? end ``` NOTE: These callbacks are called only when the associated objects are added or -removed through the association collection: +removed through the association collection. ```ruby # Triggers `before_add` callback @@ -786,7 +793,7 @@ class Order < ApplicationRecord end ``` -As the proc is evaluated in the context of the object, it is also possible to +Since the proc is evaluated in the context of the object, it is also possible to write this as: ```ruby From 4f67b16cee56d74c61ef965859a1e3869360f910 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 3 Apr 2024 10:06:28 +0200 Subject: [PATCH 07/86] move the sentence below the code example --- guides/source/active_record_callbacks.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 05a2c5bc3d453..7bba3c9362c03 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -714,7 +714,7 @@ array: ```ruby class Author < ApplicationRecord has_many :books, - before_add: [:check_limit, :calculate_shipping_charges] + before_add: [:check_limit, :calculate_shipping_charges] def check_limit(book) # ... @@ -727,8 +727,7 @@ end ``` If a `before_add` callback throws `:abort`, the object does not get added to the -collection. Similarly, if a `before_remove` callback throws `:abort`, the object -does not get removed from the collection: +collection. ```ruby # book won't be added if the limit has been reached @@ -737,6 +736,9 @@ def check_limit(book) end ``` +Similarly, if a `before_remove` callback throws `:abort`, the object +does not get removed from the collection. + NOTE: These callbacks are called only when the associated objects are added or removed through the association collection. From d86f10401fdd3b18417d35d67645c55fb12796e6 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 3 Apr 2024 10:40:24 +0200 Subject: [PATCH 08/86] update the transactional callback ordering --- guides/source/active_record_callbacks.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 7bba3c9362c03..a67301985953c 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -692,7 +692,7 @@ You can define association callbacks by adding options to the association. Suppose you have an example where an author can have many books. However, before adding a book to the authors collection, you want to ensure that the author has not reached their book limit. You can do this by adding a `before_add` callback -to the `Author` model for the `has_many: books` association. +to check the limit. ```ruby class Author < ApplicationRecord @@ -1036,9 +1036,11 @@ User was saved to database ### Transactional Callback Ordering -By default, callbacks will run in the order they are defined. However, when -defining multiple transactional `after_` callbacks (`after_commit`, -`after_rollback`, etc), the order can be reversed from when they are defined. +By default (from Rails 7.1), callbacks will run in the order they are defined. + +However, in prior versions of Rails, when defining multiple transactional +`after_` callbacks (`after_commit`, `after_rollback`, etc), the order in which +the callbacks were run was reversed. ```ruby class User < ActiveRecord::Base @@ -1047,18 +1049,16 @@ class User < ActiveRecord::Base end ``` -NOTE: This applies to all `after_*_commit` variations too, such as -`after_destroy_commit`. - -This order can be set via configuration: +We can change that reverse behaviour by setting the [following +configuration](configuring.html#config-active-record-run-after-transaction-callbacks-in-order-defined) +to `true`. The callbacks will then run in the order they are defined. ```ruby -config.active_record.run_after_transaction_callbacks_in_order_defined = false +config.active_record.run_after_transaction_callbacks_in_order_defined = true ``` -When set to `true` (the default from Rails 7.1), callbacks are executed in the -order they are defined. When set to `false`, the order is reversed, just like in -the example above. +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 From 6009818e59eca100548b9a4ab19f60f02c369551 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 10 Apr 2024 11:42:42 +0200 Subject: [PATCH 09/86] updates based on feedback --- guides/source/active_record_callbacks.md | 72 ++++++++++++++++++++---- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index a67301985953c..55a25c1636933 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -110,6 +110,8 @@ class AddUsername end ``` +### Registering Callbacks to Fire on Lifecycle Events + 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. @@ -194,8 +196,9 @@ combination. #### Validation Callbacks -Validation callbacks are triggered by the `valid?` method. They can be called -before and after the validation phase. +Validation callbacks are triggered whenever the record is validated directly via +the [`valid?`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) or [`validate`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) methods, or indirectly via `create`, `update`, or +`save`. They are called before and after the validation phase. ```ruby class User < ApplicationRecord @@ -310,6 +313,52 @@ User welcome email sent to: john.doe@example.com => # ``` +`after_commit` / `after_rollback` examples can be found +[here](active_record_callbacks.html#after-commit-and-after-rollback). + +#### Using a combination of callbacks + +Often, you will need to use a combination of callbacks to achieve the desired +behavior. For example, you may want to send a confirmation email after a user is +created, but only if the user is new and not being updated. When a user user is +updated, you may want to notify an admin if critical information is changed. In +this case, you can use `after_create` and `after_update` callbacks together. + +```ruby +class User < ApplicationRecord + after_create :send_confirmation_email + after_update :notify_admin_if_critical_info_updated + + private + def generate_confirmation_token + self.confirmation_token = SecureRandom.hex(10) + Rails.logger.info("Confirmation token generated for: #{email}") + end + + def send_confirmation_email + UserMailer.confirmation_email(self).deliver_later + Rails.logger.info("Confirmation email sent to: #{email}") + end + + def notify_admin_if_critical_info_updated + if saved_change_to_email? || saved_change_to_phone_number? + AdminMailer.user_critical_info_updated(self).deliver_later + Rails.logger.info("Notification sent to admin about critical info update for: #{email}") + end + end +end +``` + +```irb +irb> user = User.create(name: "John Doe", email: "john.doe@example.com") +Confirmation email sent to: john.doe@example.com +=> # + +irb> user.update(email: "john.doe.new@example.com") +Notification sent to admin about critical info update for: john.doe@example.com +=> true +``` + ### Updating an Object * [`before_validation`][] @@ -396,13 +445,6 @@ 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. However, anything that happens `after_commit` can guarantee the -transaction has already been completed and the data was persisted to the -database. More on [transactional callbacks](#transaction-callbacks) below. - ```ruby class User < ApplicationRecord before_destroy :check_admin_count @@ -412,7 +454,7 @@ class User < ApplicationRecord private def check_admin_count if User.where(admin: true).count == 1 && admin? - raise StandardError.new("Cannot delete the last admin user") + throw :abort end Rails.logger.info("Checked the admin count") end @@ -424,6 +466,7 @@ class User < ApplicationRecord end def notify_users + UserMailer.deletion_email(self).deliver_later Rails.logger.info("Notification sent to other users about user deletion") end end @@ -491,6 +534,8 @@ You have initialized an object! The [`after_touch`][] callback will be called whenever an Active Record object is touched. +NOTE: You can read more about `touch` [here](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-touch) + ```ruby class User < ApplicationRecord after_touch do |user| @@ -965,6 +1010,13 @@ object associated with the same database record. It can influence the flow and predictability of callback sequences, leading to potential inconsistencies in application logic following the transaction. +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. However, anything that happens `after_commit` can guarantee the +transaction has already been completed and the data was persisted to the +database. More on [transactional callbacks](#transaction-callbacks) below. + ### Aliases for `after_commit` Since using the `after_commit` callback only on create, update, or delete is From 8f3fe4631f28f7c0e028da6e072b111f7310ce8b Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 10 Apr 2024 11:46:13 +0200 Subject: [PATCH 10/86] header guidelines --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 55a25c1636933..831a50d144f4a 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -316,7 +316,7 @@ User welcome email sent to: john.doe@example.com `after_commit` / `after_rollback` examples can be found [here](active_record_callbacks.html#after-commit-and-after-rollback). -#### Using a combination of callbacks +#### Using a Combination of Callbacks Often, you will need to use a combination of callbacks to achieve the desired behavior. For example, you may want to send a confirmation email after a user is From 98546298372aa9a57a622a0744964e871dcefad8 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 10 Apr 2024 11:47:02 +0200 Subject: [PATCH 11/86] column count --- guides/source/active_record_callbacks.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 831a50d144f4a..c9a8eaefd83bd 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -48,8 +48,8 @@ Callback Registration To use the available callbacks, you need to implement and register them. Implementation can be done in a multitude of ways like using ordinary methods, -blocks and procs, or defining custom callback objects using classes. -Let's go through each of these implementation techniques. +blocks and procs, or defining custom callback objects using classes. Let's go +through each of these implementation techniques. You can implement the callbacks as a **macro-style method that calls an ordinary method** for registration. @@ -92,7 +92,8 @@ class User < ApplicationRecord end ``` -Lastly, you can define **a custom callback object**, as shown below. We will cover these [later in more detail](#callback-objects). +Lastly, you can define **a custom callback object**, as shown below. We will +cover these [later in more detail](#callback-objects). ```ruby class User < ApplicationRecord @@ -197,8 +198,12 @@ combination. #### Validation Callbacks Validation callbacks are triggered whenever the record is validated directly via -the [`valid?`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) or [`validate`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) methods, or indirectly via `create`, `update`, or -`save`. They are called before and after the validation phase. +the +[`valid?`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) +or +[`validate`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) +methods, or indirectly via `create`, `update`, or `save`. They are called before +and after the validation phase. ```ruby class User < ApplicationRecord @@ -534,7 +539,8 @@ You have initialized an object! The [`after_touch`][] callback will be called whenever an Active Record object is touched. -NOTE: You can read more about `touch` [here](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-touch) +NOTE: You can read more about `touch` +[here](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-touch) ```ruby class User < ApplicationRecord @@ -781,8 +787,8 @@ def check_limit(book) end ``` -Similarly, if a `before_remove` callback throws `:abort`, the object -does not get removed from the collection. +Similarly, if a `before_remove` callback throws `:abort`, the object does not +get removed from the collection. NOTE: These callbacks are called only when the associated objects are added or removed through the association collection. From 7fb8b6aa54d2ac7a985bc90583297ea613a4a2ee Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 10 Apr 2024 11:59:53 +0200 Subject: [PATCH 12/86] summary of the guide --- guides/source/active_record_callbacks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index c9a8eaefd83bd..7d4b5601c1bc2 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -9,10 +9,10 @@ objects. After reading this guide, you will know: * 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 register, run, and skip callbacks that respond to these events. +* How to create relational, association, conditional, and transactional callbacks. * How to create special classes that encapsulate common behavior for your - callbacks. + callbacks to be reused. -------------------------------------------------------------------------------- From 2e1c5d80d9725fe471148871658d685991d358cd Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 10 Apr 2024 12:00:31 +0200 Subject: [PATCH 13/86] move callback objects to the end --- guides/source/active_record_callbacks.md | 117 ++++++++++++----------- 1 file changed, 59 insertions(+), 58 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 7d4b5601c1bc2..d27808144e17a 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -891,64 +891,6 @@ end The callback only runs when all the `:if` conditions and none of the `:unless` conditions are evaluated to `true`. -Callback Objects ----------------- - -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 to -deal with the cleanup 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 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. This will work on any model that uses the class -like so: - -```ruby -class PictureFile < ApplicationRecord - after_destroy FileDestroyerCallback.new -end -``` - -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 FileDestroyerCallback - def self.after_destroy(file) - if File.exist?(file.filepath) - File.delete(file.filepath) - end - end -end -``` - -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 FileDestroyerCallback -end -``` - -You can declare as many callbacks as you want inside your callback objects. Transaction Callbacks --------------------- @@ -1126,3 +1068,62 @@ NOTE: This applies to all `after_*_commit` variations too, such as 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 + +Callback Objects +---------------- + +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 to +deal with the cleanup 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 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. This will work on any model that uses the class +like so: + +```ruby +class PictureFile < ApplicationRecord + after_destroy FileDestroyerCallback.new +end +``` + +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 FileDestroyerCallback + def self.after_destroy(file) + if File.exist?(file.filepath) + File.delete(file.filepath) + end + end +end +``` + +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 FileDestroyerCallback +end +``` + +You can declare as many callbacks as you want inside your callback objects. From 482e198b7e7beb396f1dac54e090a71e0285d2c6 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 10 Apr 2024 12:03:39 +0200 Subject: [PATCH 14/86] collumn count --- guides/source/active_record_callbacks.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index d27808144e17a..5e00053600e40 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -10,7 +10,8 @@ After reading this guide, you will know: * When certain events occur during the life of an Active Record object. * How to register, run, and skip callbacks that respond to these events. -* How to create relational, association, conditional, and transactional callbacks. +* How to create relational, association, conditional, and transactional + callbacks. * How to create special classes that encapsulate common behavior for your callbacks to be reused. From 0c295cb76f633c0c58fa567f8ee9d72384ee05de Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 10 Apr 2024 13:06:58 +0200 Subject: [PATCH 15/86] make some small tweaks to the documentation --- guides/source/active_record_callbacks.md | 111 +++++++++++------------ 1 file changed, 54 insertions(+), 57 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 5e00053600e40..6733333f31f6a 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -20,9 +20,11 @@ After reading this guide, you will know: The Object Life Cycle --------------------- -During the normal operation of a Rails application, objects may be created, -updated, and destroyed. Active Record provides hooks into this *object life -cycle* so that you can control your application and its data. +During the normal operation of a Rails application, objects may be [created, +updated, and +destroyed](active_record_basics.html#crud-reading-and-writing-data). Active +Record provides hooks into this object life cycle so that you can control your +application and its data. Callbacks allow you to trigger logic before or after an alteration of an object's state. They are methods that get called at certain moments of an @@ -145,10 +147,10 @@ public, they can be called from outside of the model and violate the principle of object encapsulation. WARNING. Refrain from using methods like `update`, `save`, or any other methods -that cause side effects on the object within your callback functions. For -instance, avoid calling `update(attribute: "value")` inside a callback. This +that cause side effects on the object within your callback functions.

+For instance, avoid calling `update(attribute: "value")` inside a callback. This practice can modify the model's state and potentially lead to unforeseen side -effects during commit. Instead, you can assign values directly (e.g., +effects during commit.

Instead, you can assign values directly (e.g., `self.attribute = "value"`) in `before_create`, `before_update`, or earlier callbacks for a safer approach. @@ -193,8 +195,9 @@ order in which they will get called** during the respective operations: https://api.rubyonrails.org/classes/ActiveModel/Validations/Callbacks/ClassMethods.html#method-i-before_validation There are examples below that show how to use these callbacks. We've grouped -them by the operation they are associated with, however they can be used in any -combination. +them by the operation they are associated with, and lastly showed how they can +be used in combination. `after_commit` / `after_rollback` examples can be found +[here](active_record_callbacks.html#after-commit-and-after-rollback). #### Validation Callbacks @@ -281,8 +284,8 @@ Update Cache Create callbacks are triggered whenever the record is persisted (i.e. "saved") to the underlying database **for the first time**, in other words, when we're -saving a new record, via the `create`, `save`, or `update` methods. They are -called before, after and around the object is created. +saving a new record, via the `create` or `save` methods. They are called before, +after and around the object is created. ```ruby class User < ApplicationRecord @@ -319,9 +322,6 @@ User welcome email sent to: john.doe@example.com => # ``` -`after_commit` / `after_rollback` examples can be found -[here](active_record_callbacks.html#after-commit-and-after-rollback). - #### Using a Combination of Callbacks Often, you will need to use a combination of callbacks to achieve the desired @@ -538,10 +538,8 @@ You have initialized an object! ### `after_touch` The [`after_touch`][] callback will be called whenever an Active Record object -is touched. - -NOTE: You can read more about `touch` -[here](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-touch) +is touched. You can read more about `touch` +[here](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-touch). ```ruby class User < ApplicationRecord @@ -677,7 +675,7 @@ execution. This queue will include all of your model's validations, the registered callbacks, and the database operation to be executed. The whole callback chain is wrapped in a transaction. If any callback raises an -exception, the execution chain gets halted and a ROLLBACK is issued. To +exception, the execution chain gets halted and a **rollback** is issued. To intentionally halt a chain use: ```ruby @@ -751,40 +749,41 @@ class Author < ApplicationRecord has_many :books, before_add: :check_limit private - def check_limit(book) - # ... + def check_limit + if books.count >= 5 + errors.add(:base, "Cannot add more than 5 books for this author") + throw(:abort) + end end end ``` -Rails passes the object being added or removed to the callback. +If a `before_add` callback throws `:abort`, the object does not get added to the +collection. At times you may want to perform multiple actions on the associated object. In this case, you can stack callbacks on a single event by passing them as an -array: +array. Additionally, Rails passes the object being added or removed to the +callback for you to use. ```ruby class Author < ApplicationRecord has_many :books, before_add: [:check_limit, :calculate_shipping_charges] - def check_limit(book) - # ... + def check_limit + if books.count >= 5 + errors.add(:base, "Cannot add more than 5 books for this author") + throw(:abort) + end end def calculate_shipping_charges(book) - # ... - end -end -``` + weight_in_pounds = book.weight_in_pounds || 1 + shipping_charges = weight_in_pounds * 2 -If a `before_add` callback throws `:abort`, the object does not get added to the -collection. - -```ruby -# book won't be added if the limit has been reached -def check_limit(book) - throw(:abort) if limit_reached? + shipping_charges + end end ``` @@ -942,29 +941,27 @@ transaction. However, if an exception is raised within one of these callbacks, the exception will bubble up and any remaining `after_commit` or `after_rollback` methods will _not_ be executed. As such, if your callback code could raise an exception, you'll need to rescue it and handle it within the -callback in order to allow other callbacks to run. - -WARNING. The code executed within `after_commit` or `after_rollback` callbacks -is itself not enclosed within a transaction. - -WARNING. In the context of a single transaction, if you interact with multiple -loaded objects that represent the same record in the database, there's a crucial -behavior in the `after_commit` and `after_rollback` callbacks to note. These -callbacks are triggered only for the first object of the specific record that -changes within the transaction. Other loaded objects, despite representing the -same database record, will not have their respective `after_commit` or -`after_rollback` callbacks triggered. This nuanced behavior is particularly -impactful in scenarios where you expect independent callback execution for each -object associated with the same database record. It can influence the flow and +callback in order to allow other callbacks to run.

`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. However, +anything that happens `after_commit` can guarantee the transaction has already +been completed and the data was persisted to the database. More on +[transactional callbacks](#transaction-callbacks) below.

In the context +of a single transaction, if you interact with multiple loaded objects that +represent the same record in the database, there's a crucial behavior in the +`after_commit` and `after_rollback` callbacks to note. These callbacks are +triggered only for the first object of the specific record that changes within +the transaction. Other loaded objects, despite representing the same database +record, will not have their respective `after_commit` or `after_rollback` +callbacks triggered. This nuanced behavior is particularly impactful in +scenarios where you expect independent callback execution for each object +associated with the same database record. It can influence the flow and predictability of callback sequences, leading to potential inconsistencies in -application logic following the transaction. - -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. However, anything that happens `after_commit` can guarantee the -transaction has already been completed and the data was persisted to the -database. More on [transactional callbacks](#transaction-callbacks) below. +application logic following the transaction.

Also note that the code +executed within `after_commit` or `after_rollback` callbacks is itself not +enclosed within a transaction. + ### Aliases for `after_commit` From 9e7f92459a6c8b8d634a70f13705b7da08a3985a Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:34:29 +0200 Subject: [PATCH 16/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 6733333f31f6a..f52b00e07f8ce 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -26,7 +26,7 @@ destroyed](active_record_basics.html#crud-reading-and-writing-data). Active Record provides hooks into this object life cycle so that you can control your application and its data. -Callbacks allow you to trigger logic before or after an alteration of an +Callbacks allow you to trigger logic before or after a change to an object's state. They are methods that get called at certain moments of an object's life cycle. With callbacks it is possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, From fb86d3d07db7f19acb637a03488a243b3f6c516e Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:35:15 +0200 Subject: [PATCH 17/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index f52b00e07f8ce..181e2cc56eea0 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -29,7 +29,7 @@ application and its data. Callbacks allow you to trigger logic before or after a change to an object's state. They are methods that get called at certain moments of an object's life cycle. With callbacks it is possible to write code that will run -whenever an Active Record object is created, saved, updated, deleted, validated, +whenever an Active Record object is initialized, created, saved, updated, deleted, validated, or loaded from the database. ```ruby From 2d2d618a62c280cc8448fae377b7e4c7d3dead58 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:35:23 +0200 Subject: [PATCH 18/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 181e2cc56eea0..7193787f7abda 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -47,7 +47,7 @@ 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. Callback Registration ------------------- +--------------------- To use the available callbacks, you need to implement and register them. Implementation can be done in a multitude of ways like using ordinary methods, From 039fd2e4b5a5760ec3aa3209a9487cc1e1d75611 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:36:23 +0200 Subject: [PATCH 19/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 7193787f7abda..d81b0ac963d20 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -195,7 +195,7 @@ order in which they will get called** during the respective operations: https://api.rubyonrails.org/classes/ActiveModel/Validations/Callbacks/ClassMethods.html#method-i-before_validation There are examples below that show how to use these callbacks. We've grouped -them by the operation they are associated with, and lastly showed how they can +them by the operation they are associated with, and lastly show how they can be used in combination. `after_commit` / `after_rollback` examples can be found [here](active_record_callbacks.html#after-commit-and-after-rollback). From 91a2948b90b9786390e7e0e64e4707a6b4f44801 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:37:22 +0200 Subject: [PATCH 20/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index d81b0ac963d20..78d9781e97f74 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -326,7 +326,7 @@ User welcome email sent to: john.doe@example.com Often, you will need to use a combination of callbacks to achieve the desired behavior. For example, you may want to send a confirmation email after a user is -created, but only if the user is new and not being updated. When a user user is +created, but only if the user is new and not being updated. When a user is updated, you may want to notify an admin if critical information is changed. In this case, you can use `after_create` and `after_update` callbacks together. From 18c2bc3e85e1af7c52aa76f04a025d69f6e419ba Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:46:40 +0200 Subject: [PATCH 21/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 78d9781e97f74..98beb3f08b3db 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -842,7 +842,7 @@ option is best suited when writing short validation methods, usually one-liners: ```ruby class Order < ApplicationRecord before_save :normalize_card_number, - if: proc { |order| order.paid_with_card? } + if: -> { |order| order.paid_with_card? } end ``` From f7a2eeeaab8a9325869f8da8ce7300db6d191874 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:46:47 +0200 Subject: [PATCH 22/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 98beb3f08b3db..a95b42f9c095c 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -851,7 +851,7 @@ write this as: ```ruby class Order < ApplicationRecord - before_save :normalize_card_number, if: proc { paid_with_card? } + before_save :normalize_card_number, if: -> { paid_with_card? } end ``` From 2821627644262c0f3d2d3db139b54d36a48bf3e9 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:46:52 +0200 Subject: [PATCH 23/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index a95b42f9c095c..4617fd87eb420 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -883,8 +883,8 @@ Callbacks can mix both `:if` and `:unless` in the same declaration: ```ruby class Comment < ApplicationRecord before_save :filter_content, - if: proc { forum.parental_control? }, - unless: proc { author.trusted? } + if: -> { forum.parental_control? }, + unless: -> { author.trusted? } end ``` From 7308c8a2c4a3acb556e3a552b1263450a66d1685 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:47:14 +0200 Subject: [PATCH 24/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 4617fd87eb420..2c565606718a3 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -447,7 +447,7 @@ Update email sent to: john.doe@example.com [`before_destroy`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_destroy -NOTE: `before_destroy` callbacks should be placed before `dependent: :destroy` +WARNING: `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`. From 26a22f49f2c74ff63a7889bc4a70c1cba8cd4c16 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:47:22 +0200 Subject: [PATCH 25/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 2c565606718a3..97bc73d054abc 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -872,7 +872,7 @@ 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 { untrusted_author? }] + if: [:subject_to_parental_control?, -> { untrusted_author? }] end ``` From 3c12c32a1317fd52a7d258fa6b06042c07b1fe1d Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 21:50:36 +0200 Subject: [PATCH 26/86] add before to relation --- guides/source/active_record_callbacks.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 97bc73d054abc..bb69f47ff9bbf 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -768,8 +768,7 @@ callback for you to use. ```ruby class Author < ApplicationRecord - has_many :books, - before_add: [:check_limit, :calculate_shipping_charges] + has_many :books, before_add: [:check_limit, :calculate_shipping_charges] def check_limit if books.count >= 5 From 7a3e44c86cbf6bff7bf86053e456e9a6d5de8c81 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 22:01:10 +0200 Subject: [PATCH 27/86] replace puts with Rails.logger.info --- guides/source/active_record_callbacks.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index bb69f47ff9bbf..a2adb31d1fce9 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -34,7 +34,7 @@ or loaded from the database. ```ruby class Baby < ApplicationRecord - after_create -> { puts "Congratulations!" } + after_create -> { Rails.logger.info("Congratulations!") } end ``` @@ -510,11 +510,11 @@ They can be registered just like the other Active Record callbacks. ```ruby class User < ApplicationRecord after_initialize do |user| - puts "You have initialized an object!" + Rails.logger.info("You have initialized an object!") end after_find do |user| - puts "You have found an object!" + Rails.logger.info("You have found an object!") end end ``` @@ -544,7 +544,7 @@ is touched. You can read more about `touch` ```ruby class User < ApplicationRecord after_touch do |user| - puts "You have touched an object" + Rails.logger.info("You have touched an object") end end ``` @@ -564,7 +564,7 @@ It can be used along with `belongs_to`: class Book < ApplicationRecord belongs_to :library, touch: true after_touch do - puts 'A Book was touched' + Rails.logger.info('A Book was touched') end end @@ -574,7 +574,7 @@ class Library < ApplicationRecord private def log_when_books_or_library_touched - puts 'Book/Library was touched' + Rails.logger.info('Book/Library was touched') end end ``` @@ -710,7 +710,7 @@ class Article < ApplicationRecord after_destroy :log_destroy_action def log_destroy_action - puts 'Article destroyed' + Rails.logger.info('Article destroyed') end end ``` @@ -995,7 +995,7 @@ class User < ApplicationRecord private def log_user_saved_to_db - puts 'User was saved to database' + Rails.logger.info('User was saved to database') end end ``` @@ -1018,7 +1018,7 @@ class User < ApplicationRecord private def log_user_saved_to_db - puts 'User was saved to database' + Rails.logger.info('User was saved to database') end end ``` @@ -1041,8 +1041,8 @@ the callbacks were run was reversed. ```ruby class User < ActiveRecord::Base - after_commit { puts("this actually gets called second") } - after_commit { puts("this actually gets called first") } + after_commit { Rails.logger.info("this actually gets called second") } + after_commit { Rails.logger.info("this actually gets called first") } end ``` From cb1b4717fc63d8a72776d9fb14dcaf6f81268c18 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 22:05:14 +0200 Subject: [PATCH 28/86] change single quotes to double quotes --- guides/source/active_record_callbacks.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index a2adb31d1fce9..8109d2d12865f 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -550,7 +550,7 @@ end ``` ```irb -irb> user = User.create(name: 'Kuldeep') +irb> user = User.create(name: "Kuldeep") => # irb> user.touch @@ -564,7 +564,7 @@ It can be used along with `belongs_to`: class Book < ApplicationRecord belongs_to :library, touch: true after_touch do - Rails.logger.info('A Book was touched') + Rails.logger.info("A Book was touched") end end @@ -574,7 +574,7 @@ class Library < ApplicationRecord private def log_when_books_or_library_touched - Rails.logger.info('Book/Library was touched') + Rails.logger.info("Book/Library was touched") end end ``` @@ -710,7 +710,7 @@ class Article < ApplicationRecord after_destroy :log_destroy_action def log_destroy_action - Rails.logger.info('Article destroyed') + Rails.logger.info("Article destroyed") end end ``` @@ -995,7 +995,7 @@ class User < ApplicationRecord private def log_user_saved_to_db - Rails.logger.info('User was saved to database') + Rails.logger.info("User was saved to database") end end ``` @@ -1018,7 +1018,7 @@ class User < ApplicationRecord private def log_user_saved_to_db - Rails.logger.info('User was saved to database') + Rails.logger.info("User was saved to database") end end ``` From 95011582e6ea0cc792a5ac1a929479295ec6768f Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 22:14:34 +0200 Subject: [PATCH 29/86] change the method name to not include normalize --- guides/source/active_record_callbacks.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 8109d2d12865f..b035a1d688187 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -212,13 +212,13 @@ and after the validation phase. ```ruby class User < ApplicationRecord validates :name, presence: true - before_validation :normalize_name + before_validation :titleize_name after_validation :check_errors private - def normalize_name + def titleize_name self.name = name.downcase.titleize if name.present? - Rails.logger.info("Name normalized to #{name}") + Rails.logger.info("Name titleized to #{name}") end def check_errors @@ -234,7 +234,7 @@ irb> user = User.new(name: "", email: "john.doe@example.com", password: "abc1234 => # irb> user.valid? -Name normalized to +Name titleized to Validation failed: Name can't be blank => false ``` From 5dbbe0ba6fca9247a6e38ef4f884d5e11f33995d Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 12 Apr 2024 22:32:35 +0200 Subject: [PATCH 30/86] add a note about valid? --- guides/source/active_record_callbacks.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index b035a1d688187..622071fe8c5c7 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -209,6 +209,8 @@ or methods, or indirectly via `create`, `update`, or `save`. They are called before and after the validation phase. +NOTE: `validate` is an alias for [`valid?`](https://api.rubyonrails.org/classes/ActiveRecord/Validations.html#method-i-valid-3F). + ```ruby class User < ApplicationRecord validates :name, presence: true From 928df1e81b0e4eddbecbaffdcaa027e9a3303dde Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 12:33:12 +0200 Subject: [PATCH 31/86] Update guides/source/active_record_callbacks.md Co-authored-by: Carlos Antonio da Silva --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 622071fe8c5c7..17d798b10a805 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -461,7 +461,7 @@ class User < ApplicationRecord private def check_admin_count - if User.where(admin: true).count == 1 && admin? + if admin? && User.where(role: "admin").count == 1 throw :abort end Rails.logger.info("Checked the admin count") From 6272294d91982f8e999fd6c8b2e642ebfa7f9739 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 12:33:42 +0200 Subject: [PATCH 32/86] Update guides/source/active_record_callbacks.md Co-authored-by: hatsu --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 17d798b10a805..430fef7843489 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -205,7 +205,7 @@ Validation callbacks are triggered whenever the record is validated directly via the [`valid?`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) or -[`validate`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) +[`validate`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-validate) methods, or indirectly via `create`, `update`, or `save`. They are called before and after the validation phase. From 6f913cedb71a247e38add69064934fe4fda019b8 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 12:34:08 +0200 Subject: [PATCH 33/86] Update guides/source/active_record_callbacks.md Co-authored-by: hatsu --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 430fef7843489..770760a0ce85b 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -363,7 +363,7 @@ Confirmation email sent to: john.doe@example.com => # irb> user.update(email: "john.doe.new@example.com") -Notification sent to admin about critical info update for: john.doe@example.com +Notification sent to admin about critical info update for: john.doe.new@example.com => true ``` From 8abf2c33193b0e03eec1a907a823e12444450ddd Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 12:34:29 +0200 Subject: [PATCH 34/86] Update guides/source/active_record_callbacks.md Co-authored-by: hatsu --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 770760a0ce85b..eb8dd709fd0ea 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -426,7 +426,7 @@ end ```irb irb> user = User.find(1) -=> # +=> # irb> user.update(role: "admin") User role changed to admin From e59514500668fa962c5db4a84bca2e5e67fd969f Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 12:37:00 +0200 Subject: [PATCH 35/86] Update guides/source/active_record_callbacks.md Co-authored-by: Carlos Antonio da Silva --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index eb8dd709fd0ea..cb93f1c068772 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -255,7 +255,7 @@ class User < ApplicationRecord private def encrypt_password - self.password = BCrypt::Password.create(password) + self.password_digest = BCrypt::Password.create(password) Rails.logger.info("Password encrypted for user with email: #{email}") end From 9249abb4c36c05d8daa0d86828264fa0e3a72565 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 12:37:31 +0200 Subject: [PATCH 36/86] Update guides/source/active_record_callbacks.md Co-authored-by: Carlos Antonio da Silva --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index cb93f1c068772..b81aeffc9f76a 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1035,7 +1035,7 @@ User was saved to database ### Transactional Callback Ordering -By default (from Rails 7.1), callbacks will run in the order they are defined. +By default (from Rails 7.1), transaction callbacks will run in the order they are defined. However, in prior versions of Rails, when defining multiple transactional `after_` callbacks (`after_commit`, `after_rollback`, etc), the order in which From 372df42dd1692c99c29b2554e69dca8f81f95770 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 15:17:43 +0200 Subject: [PATCH 37/86] make updates based on feedbcak --- guides/source/active_record_callbacks.md | 122 +++++++++++------------ 1 file changed, 57 insertions(+), 65 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index b81aeffc9f76a..3f29dfc3a2b93 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -12,7 +12,7 @@ After reading this guide, you will know: * How to register, run, and skip callbacks that respond to these events. * How to create relational, association, conditional, and transactional callbacks. -* How to create special classes that encapsulate common behavior for your +* How to create objects that encapsulate common behavior for your callbacks to be reused. -------------------------------------------------------------------------------- @@ -34,13 +34,13 @@ or loaded from the database. ```ruby class Baby < ApplicationRecord - after_create -> { Rails.logger.info("Congratulations!") } + after_create -> { Rails.logger.info("Congratulations, the callback has run!") } end ``` ```irb irb> baby = Baby.create -Congratulations! +Congratulations, the callback has run! ``` As you will see, there are many life cycle events and you can choose to hook @@ -51,8 +51,8 @@ Callback Registration To use the available callbacks, you need to implement and register them. Implementation can be done in a multitude of ways like using ordinary methods, -blocks and procs, or defining custom callback objects using classes. Let's go -through each of these implementation techniques. +blocks and procs, or defining custom callback objects using classes or modules. +Let's go through each of these implementation techniques. You can implement the callbacks as a **macro-style method that calls an ordinary method** for registration. @@ -142,9 +142,9 @@ class User < ApplicationRecord 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. +NOTE: 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. Refrain from using methods like `update`, `save`, or any other methods that cause side effects on the object within your callback functions.

@@ -204,18 +204,16 @@ be used in combination. `after_commit` / `after_rollback` examples can be found Validation callbacks are triggered whenever the record is validated directly via the [`valid?`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) -or -[`validate`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-validate) -methods, or indirectly via `create`, `update`, or `save`. They are called before +( or its alias +[`validate`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-validate)) +or [`invalid?`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-invalid-3F) method, or indirectly via `create`, `update`, or `save`. They are called before and after the validation phase. -NOTE: `validate` is an alias for [`valid?`](https://api.rubyonrails.org/classes/ActiveRecord/Validations.html#method-i-valid-3F). - ```ruby class User < ApplicationRecord validates :name, presence: true before_validation :titleize_name - after_validation :check_errors + after_validation :log_errors private def titleize_name @@ -223,7 +221,7 @@ class User < ApplicationRecord Rails.logger.info("Name titleized to #{name}") end - def check_errors + def log_errors if errors.any? Rails.logger.error("Validation failed: #{errors.full_messages.join(', ')}") end @@ -266,14 +264,14 @@ class User < ApplicationRecord end def update_cache - Rails.cache.write("user_data", attributes) + Rails.cache.write(["user_data", self], attributes) Rails.logger.info("Update Cache") end end ``` ```irb -irb> user = User.create(name: "Jane Doe", email: "jane.doe@example.com") +irb> user = User.create(name: "Jane Doe", password: "password", email: "jane.doe@example.com") Password encrypted for user with email: jane.doe@example.com Saving user with email: jane.doe@example.com @@ -323,50 +321,6 @@ User created with email: john.doe@example.com User welcome email sent to: john.doe@example.com => # ``` - -#### Using a Combination of Callbacks - -Often, you will need to use a combination of callbacks to achieve the desired -behavior. For example, you may want to send a confirmation email after a user is -created, but only if the user is new and not being updated. When a user is -updated, you may want to notify an admin if critical information is changed. In -this case, you can use `after_create` and `after_update` callbacks together. - -```ruby -class User < ApplicationRecord - after_create :send_confirmation_email - after_update :notify_admin_if_critical_info_updated - - private - def generate_confirmation_token - self.confirmation_token = SecureRandom.hex(10) - Rails.logger.info("Confirmation token generated for: #{email}") - end - - def send_confirmation_email - UserMailer.confirmation_email(self).deliver_later - Rails.logger.info("Confirmation email sent to: #{email}") - end - - def notify_admin_if_critical_info_updated - if saved_change_to_email? || saved_change_to_phone_number? - AdminMailer.user_critical_info_updated(self).deliver_later - Rails.logger.info("Notification sent to admin about critical info update for: #{email}") - end - end -end -``` - -```irb -irb> user = User.create(name: "John Doe", email: "john.doe@example.com") -Confirmation email sent to: john.doe@example.com -=> # - -irb> user.update(email: "john.doe.new@example.com") -Notification sent to admin about critical info update for: john.doe.new@example.com -=> true -``` - ### Updating an Object * [`before_validation`][] @@ -435,6 +389,44 @@ User updated with email: john.doe@example.com Update email sent to: john.doe@example.com ``` +#### Using a Combination of Callbacks + +Often, you will need to use a combination of callbacks to achieve the desired +behavior. For example, you may want to send a confirmation email after a user is +created, but only if the user is new and not being updated. When a user is +updated, you may want to notify an admin if critical information is changed. In +this case, you can use `after_create` and `after_update` callbacks together. + +```ruby +class User < ApplicationRecord + after_create :send_confirmation_email + after_update :notify_admin_if_critical_info_updated + + private + def send_confirmation_email + UserMailer.confirmation_email(self).deliver_later + Rails.logger.info("Confirmation email sent to: #{email}") + end + + def notify_admin_if_critical_info_updated + if saved_change_to_email? || saved_change_to_phone_number? + AdminMailer.user_critical_info_updated(self).deliver_later + Rails.logger.info("Notification sent to admin about critical info update for: #{email}") + end + end +end +``` + +```irb +irb> user = User.create(name: "John Doe", email: "john.doe@example.com") +Confirmation email sent to: john.doe@example.com +=> # + +irb> user.update(email: "john.doe.new@example.com") +Notification sent to admin about critical info update for: john.doe.new@example.com +=> true +``` + ### Destroying an Object * [`before_destroy`][] @@ -492,12 +484,12 @@ Notification sent to other users about user deletion ``` `after_commit` / `after_rollback` examples can be found -[here](active_record_callbacks.html#after-commit-and-after-rollback). +[here](#after-commit-and-after-rollback). ### `after_initialize` and `after_find` Whenever an Active Record object is instantiated, either by directly using `new` -or when a record is loaded from the database, then the [`after_initialize`][] +or when a record is loaded from the database, the [`after_initialize`][] callback will be called. It can be useful to avoid the need to directly override your Active Record `initialize` method. @@ -843,7 +835,7 @@ option is best suited when writing short validation methods, usually one-liners: ```ruby class Order < ApplicationRecord before_save :normalize_card_number, - if: -> { |order| order.paid_with_card? } + if: ->(order) { order.paid_with_card? } end ``` @@ -934,7 +926,7 @@ 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. +supply the `:on` option the callback will fire for every live cycle event. WARNING. When a transaction completes, the `after_commit` or `after_rollback` callbacks are called for all models created, updated, or destroyed within that From 7fd489059587dab99c9a7a382c70422652cc81cc Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 16:55:23 +0200 Subject: [PATCH 38/86] updates based on feeback --- guides/source/active_record_callbacks.md | 102 +++++++++++++---------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 3f29dfc3a2b93..4b76e4084060f 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -173,6 +173,9 @@ order in which they will get called** during the respective operations: * [`after_save`][] * [`after_commit`][] / [`after_rollback`][] +`after_commit` / `after_rollback` examples can be found +[here](active_record_callbacks.html#after-commit-and-after-rollback). + [`after_create`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-after_create [`after_commit`]: @@ -196,8 +199,7 @@ order in which they will get called** during the respective operations: There are examples below that show how to use these callbacks. We've grouped them by the operation they are associated with, and lastly show how they can -be used in combination. `after_commit` / `after_rollback` examples can be found -[here](active_record_callbacks.html#after-commit-and-after-rollback). +be used in combination. #### Validation Callbacks @@ -340,11 +342,13 @@ User welcome email sent to: john.doe@example.com [`before_update`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_update - -WARNING. The `after_save` callback is triggered on both a create and update. -However, it consistently executes after the more specific callbacks +WARNING: The `after_save` callback is triggered on both create and update +operations. However, it consistently executes after the more specific callbacks `after_create` and `after_update`, regardless of the sequence in which the macro -calls were made. +calls were made. Similarly, before and around save callbacks follow the same +rule: `before_save` runs before create/update, and `around_save` runs around +create/update operations. It's important to note that save callbacks will always +run before/around/after the more specific create/update callbacks. We've already covered [validation](#validation-callbacks) and [save](#save-callbacks) callbacks. `after_commit` / `after_rollback` examples @@ -441,9 +445,10 @@ Notification sent to admin about critical info update for: john.doe.new@example. [`before_destroy`]: https://api.rubyonrails.org/classes/ActiveRecord/Callbacks/ClassMethods.html#method-i-before_destroy -WARNING: `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`. +`after_commit` / `after_rollback` examples can be found +[here](#after-commit-and-after-rollback). + +#### Destroy Callbacks ```ruby class User < ApplicationRecord @@ -483,9 +488,6 @@ User with ID 1 destroyed successfully Notification sent to other users about user deletion ``` -`after_commit` / `after_rollback` examples can be found -[here](#after-commit-and-after-rollback). - ### `after_initialize` and `after_find` Whenever an Active Record object is instantiated, either by directly using `new` @@ -719,6 +721,10 @@ Article destroyed => # ``` +WARNING: When using a `before_destroy` callback, it 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`. + Association Callbacks --------------------- @@ -884,7 +890,6 @@ end The callback only runs when all the `:if` conditions and none of the `:unless` conditions are evaluated to `true`. - Transaction Callbacks --------------------- @@ -897,12 +902,26 @@ 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 that are not part of the database transaction. -Consider if the `PictureFile` model, from the previous example, 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, then 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. +Consider a `PictureFile` model that needs to delete a file after the +corresponding record is destroyed. + +```ruby +class PictureFile < ApplicationRecord + after_destroy :delete_picture_file_from_disk + + def delete_picture_file_from_disk + if File.exist?(filepath) + File.delete(filepath) + end + end +end +``` + +If anything raises an exception after the +`after_destroy` callback is called and the transaction rolls back, then 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. ```ruby PictureFile.transaction do @@ -938,23 +957,21 @@ callback in order to allow other callbacks to run.

`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. However, -anything that happens `after_commit` can guarantee the transaction has already -been completed and the data was persisted to the database. More on -[transactional callbacks](#transaction-callbacks) below.

In the context -of a single transaction, if you interact with multiple loaded objects that -represent the same record in the database, there's a crucial behavior in the -`after_commit` and `after_rollback` callbacks to note. These callbacks are -triggered only for the first object of the specific record that changes within -the transaction. Other loaded objects, despite representing the same database -record, will not have their respective `after_commit` or `after_rollback` -callbacks triggered. This nuanced behavior is particularly impactful in -scenarios where you expect independent callback execution for each object -associated with the same database record. It can influence the flow and -predictability of callback sequences, leading to potential inconsistencies in -application logic following the transaction.

Also note that the code -executed within `after_commit` or `after_rollback` callbacks is itself not -enclosed within a transaction. - +during `after_commit` the data was already persisted to the database, and thus any +exception won't roll anything back anymore. Also note that the code executed +within `after_commit` or `after_rollback` callbacks is itself not enclosed within a +transaction.

In the context of a single transaction, if you interact +with multiple loaded objects that represent the same record in the database, +there's a crucial behavior in the `after_commit` and `after_rollback` callbacks +to note. These callbacks are triggered only for the first object of the specific +record that changes within the transaction. Other loaded objects, despite +representing the same database record, will not have their respective +`after_commit` or `after_rollback` callbacks triggered. This nuanced behavior is +particularly impactful in scenarios where you expect independent callback +execution for each object associated with the same database record. It can +influence the flow and predictability of callback sequences, leading to +potential inconsistencies in application logic following the +transaction.

### Aliases for `after_commit` @@ -980,7 +997,8 @@ end WARNING. Using both `after_create_commit` and `after_update_commit` with the same method name will only allow the last callback defined to take effect, as they both internally alias to `after_commit` which overrides previously defined -callbacks with the same method name. +callbacks with the same method name. In this case, it's better to use +`after_save_commit` instead. ```ruby class User < ApplicationRecord @@ -1067,7 +1085,7 @@ 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 to +Here's an example where we create a class with an `after_commit` callback to deal with the cleanup 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 @@ -1075,7 +1093,7 @@ behavior and changing it much easier. ```ruby class FileDestroyerCallback - def after_destroy(file) + def after_commit(file) if File.exist?(file.filepath) File.delete(file.filepath) end @@ -1089,7 +1107,7 @@ like so: ```ruby class PictureFile < ApplicationRecord - after_destroy FileDestroyerCallback.new + after_commit FileDestroyerCallback.new end ``` @@ -1100,7 +1118,7 @@ it will make more sense to declare the callbacks as class methods: ```ruby class FileDestroyerCallback - def self.after_destroy(file) + def self.after_commit(file) if File.exist?(file.filepath) File.delete(file.filepath) end @@ -1113,7 +1131,7 @@ instantiate a new `FileDestroyerCallback` object in our model. ```ruby class PictureFile < ApplicationRecord - after_destroy FileDestroyerCallback + after_commit FileDestroyerCallback end ``` From 6d72caa61ef907312acb8649db60c31346b5f2d8 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 17:02:23 +0200 Subject: [PATCH 39/86] updates based on feeback --- guides/source/active_record_callbacks.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 4b76e4084060f..8778afd189261 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -117,8 +117,8 @@ end ### Registering Callbacks to Fire on Lifecycle Events 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. +can be done using the `:on` option and allows complete control over when and in +what context your callbacks are triggered. ```ruby class User < ApplicationRecord @@ -945,7 +945,8 @@ 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 live cycle event. +supply the `:on` option the callback will fire for every live cycle event. Read +more about `:on` [here](#registering-callbacks-to-fire-on-lifecycle-events. WARNING. When a transaction completes, the `after_commit` or `after_rollback` callbacks are called for all models created, updated, or destroyed within that From 42a53e1775e6b0db38e8b52ecc21f969d1053921 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 2 May 2024 19:30:08 +0200 Subject: [PATCH 40/86] lint --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 8778afd189261..8587812ab1d90 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -910,7 +910,7 @@ class PictureFile < ApplicationRecord after_destroy :delete_picture_file_from_disk def delete_picture_file_from_disk - if File.exist?(filepath) + if File.exist?(filepath) File.delete(filepath) end end From 32828b865d72e6373198952ec3dc234a7c1b716c Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 3 May 2024 12:31:12 +0200 Subject: [PATCH 41/86] fix the documentation on callback ordering --- guides/source/active_record_callbacks.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 8587812ab1d90..376b7f58d7ace 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1048,23 +1048,24 @@ User was saved to database By default (from Rails 7.1), transaction callbacks will run in the order they are defined. -However, in prior versions of Rails, when defining multiple transactional -`after_` callbacks (`after_commit`, `after_rollback`, etc), the order in which -the callbacks were run was reversed. - ```ruby class User < ActiveRecord::Base - after_commit { Rails.logger.info("this actually gets called second") } - after_commit { Rails.logger.info("this actually gets called first") } + after_commit { Rails.logger.info("this gets called first") } + after_commit { Rails.logger.info("this gets called second") } end ``` -We can change that reverse behaviour by setting the [following +However, in prior versions of Rails, when defining multiple transactional +`after_` callbacks (`after_commit`, `after_rollback`, etc), the order in which +the callbacks were run was reversed. + +If for some reason, you'd still like them to run in reverse you can set the +[following configuration](configuring.html#config-active-record-run-after-transaction-callbacks-in-order-defined) -to `true`. The callbacks will then run in the order they are defined. +to `false`. The callbacks will then run in the reverse order. ```ruby -config.active_record.run_after_transaction_callbacks_in_order_defined = true +config.active_record.run_after_transaction_callbacks_in_order_defined = false ``` NOTE: This applies to all `after_*_commit` variations too, such as From 802c601c7b3f531f5286bd0a9620407c3c00594f Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 3 May 2024 17:24:49 +0200 Subject: [PATCH 42/86] custome callbacks --- guides/source/active_record_callbacks.md | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 376b7f58d7ace..8be09e12f5883 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -120,6 +120,14 @@ Callbacks can also be registered to only fire on certain life cycle events, this can be done using the `:on` option and allows complete control over when and in what context your callbacks are triggered. +NOTE: A context is like a category or a scenario in which you want certain +validations to apply. When you validate an ActiveRecord model, you can specify a +context to group validations. This allows you to have different sets of +validations that apply in different situations. In Rails, there are certain +default contexts for validations like :create, :update, and :save. However, you +can define your own custom contexts as well. + + ```ruby class User < ApplicationRecord validates :username, :email, presence: true @@ -154,6 +162,39 @@ effects during commit.

Instead, you can assign values directly (e.g., `self.attribute = "value"`) in `before_create`, `before_update`, or earlier callbacks for a safer approach. +#### Custom Contexts + +You can also define your own custom contexts for callbacks. This can be useful +when you want to perform validations based on specific scenarios, or you want to +group certain callbacks together and run them in a specific context. + +The example below shows how to define a custom context called `:email_change` +and use it in a custom validation: + +```ruby +class User < ApplicationRecord + validate :ensure_new_email_has_value, on: :email_change + + private + + def ensure_new_email_has_value + if new_email.blank? + errors.add(:email, "can't be blank") + end + end +end +``` + +Then, you can use this custom context to trigger the validation: + +```irb +irb> user = User.last +=> # +irb> user.update_attribute(:email, "") +=> nil +irb> user.valid?(:email_change) +=> false +``` Available Callbacks ------------------- From adcc289b2c15f7feff89d603e279f1427cbd6e0f Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 3 May 2024 17:35:33 +0200 Subject: [PATCH 43/86] chore: format --- guides/source/active_record_callbacks.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 8be09e12f5883..99a2893f611d8 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -177,11 +177,11 @@ class User < ApplicationRecord private - def ensure_new_email_has_value - if new_email.blank? - errors.add(:email, "can't be blank") + def ensure_new_email_has_value + if new_email.blank? + errors.add(:email, "can't be blank") + end end - end end ``` From cd7e6f66ab6e536c22cd96b22ff507f256014f9f Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 6 May 2024 13:10:23 +0200 Subject: [PATCH 44/86] add links --- guides/source/active_record_callbacks.md | 55 ++++++++++++++++-------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 99a2893f611d8..3320592202741 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -680,30 +680,49 @@ Skipping Callbacks Just as with [validations](active_record_validations.html), it is also possible to skip callbacks by using the following methods: -* `decrement!` -* `decrement_counter` -* `delete` -* `delete_all` -* `delete_by` -* `increment!` -* `increment_counter` -* `insert` -* `insert!` -* `insert_all` -* `insert_all!` -* `touch_all` -* `update_column` -* `update_columns` -* `update_all` -* `update_counters` -* `upsert` -* `upsert_all` +* [`decrement!`][] +* [`decrement_counter`][] +* [`delete`][] +* [`delete_all`][] +* [`delete_by`][] +* [`increment!`][] +* [`increment_counter`][] +* [`insert`][] +* [`insert!`][] +* [`insert_all`][] +* [`insert_all!`][] +* [`touch_all`][] +* [`update_column`][] +* [`update_columns`][] +* [`update_all`][] +* [`update_counters`][] +* [`upsert`][] +* [`upsert_all`][] WARNING. These methods should be used with caution because there may be important business rules and application logic in callbacks that you do not want to bypass. Bypassing them without understanding the potential implications may lead to invalid data. +[`decrement!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-decrement-21 +[`decrement_counter`]: https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-decrement_counter +[`delete`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-delete +[`delete_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_all +[`delete_by`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_by +[`increment!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-increment-21 +[`increment_counter`]: https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-increment_counter +[`insert`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert +[`insert!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert-21 +[`insert_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all +[`insert_all!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all-21 +[`touch_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-touch_all +[`update_column`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column +[`update_columns`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_columns +[`update_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_all +[`update_counters`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_counters +[`upsert`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert +[`upsert_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all + Halting Execution ----------------- From d9490a0a85bb6e6f06af91cd4d4d1e0fdb89dbe6 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Tue, 7 May 2024 11:19:29 +0200 Subject: [PATCH 45/86] skipping callbacks example --- guides/source/active_record_callbacks.md | 29 ++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 3320592202741..2f566c23c18a7 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -699,6 +699,35 @@ to skip callbacks by using the following methods: * [`upsert`][] * [`upsert_all`][] +Let's consider a `User` model where the `before_save` callback logs any changes +to the user's email address: + +```ruby +class User < ApplicationRecord + before_save :log_email_change + + private + + def log_email_change + if email_changed? + Rails.logger.info("Email changed from #{email_was} to #{email}") + end + end +end +``` + +Now, suppose there's a scenario where you want to update the user's email +address without triggering the `before_save` callback to log the email change. +You can use the `update_columns` method for this purpose: + +```ruby +user = User.find(1) +user.update_columns(email: 'new_email@example.com') +``` + +The above will update the user's email address without triggering the +`before_save` callback. + WARNING. These methods should be used with caution because there may be important business rules and application logic in callbacks that you do not want to bypass. Bypassing them without understanding the potential implications may From 4a254a2a6bb6adc662ff243131cef1417a62153c Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Tue, 7 May 2024 12:15:56 +0200 Subject: [PATCH 46/86] add some more information on custom contexts --- guides/source/active_record_callbacks.md | 66 ++++++++++++++++-------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 2f566c23c18a7..1b8f7a22b7b88 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -125,8 +125,7 @@ validations to apply. When you validate an ActiveRecord model, you can specify a context to group validations. This allows you to have different sets of validations that apply in different situations. In Rails, there are certain default contexts for validations like :create, :update, and :save. However, you -can define your own custom contexts as well. - +can define [custom contexts](#custom-contexts) as well. ```ruby class User < ApplicationRecord @@ -164,36 +163,61 @@ callbacks for a safer approach. #### Custom Contexts -You can also define your own custom contexts for callbacks. This can be useful -when you want to perform validations based on specific scenarios, or you want to -group certain callbacks together and run them in a specific context. +You can define your own custom contexts for callbacks. This can be useful when +you want to perform validations based on specific scenarios, or you want to +group certain callbacks together and run them in a specific context. In these +cases you may be tempted to [skip callbacks](#skipping-callbacks) altogether, +but defining a custom context can sometimes be an alternative structured +approach. You will need to combine a `context` with the `on` option to define a +custom context for a callback. -The example below shows how to define a custom context called `:email_change` -and use it in a custom validation: +A common scenario for custom contexts is when you have a multi-step form where +you want to perform validations per step. You can define custom context for each +step of the form: ```ruby class User < ApplicationRecord - validate :ensure_new_email_has_value, on: :email_change + validate :personal_information, on: :personal_info + validate :contact_information, on: :contact_info + validate :location_information, on: :location_info private - def ensure_new_email_has_value - if new_email.blank? - errors.add(:email, "can't be blank") - end - end + def personal_information + errors.add(:base, "Name must be present") if first_name.blank? + errors.add(:base, "Age must be at least 18") if age && age < 18 + end + + def contact_information + errors.add(:base, "Email must be present") if email.blank? + errors.add(:base, "Phone number must be present") if phone.blank? + end + + def location_information + errors.add(:base, "Address must be present") if address.blank? + errors.add(:base, "City must be present") if city.blank? + end end ``` -Then, you can use this custom context to trigger the validation: +Then, you can use this custom context to trigger the validations: -```irb -irb> user = User.last -=> # -irb> user.update_attribute(:email, "") -=> nil -irb> user.valid?(:email_change) -=> false +```ruby +irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St") +irb> user.valid?(:personal_info) # => false +irb> user.valid?(:contact_info) # => true +irb> user.valid?(:location_info) # => false +``` + +You can also use the custom contexts to trigger the validations on any method +that supports callbacks. For example, you could use the custom context to +trigger the validations on `save`: + +```ruby +irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St") +irb> user.save(context: :personal_info) # => false +irb> user.save(context: :contact_info) # => true +irb> user.save(context: :location_info) # => false ``` Available Callbacks From 7ab49953c18d4499a847fdd9e21cc6862cb5a2f9 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Tue, 7 May 2024 12:45:07 +0200 Subject: [PATCH 47/86] suppress callbacks --- guides/source/active_record_callbacks.md | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 1b8f7a22b7b88..c6f37bb8128f9 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -776,6 +776,58 @@ lead to invalid data. [`upsert`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert [`upsert_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all +Suppressing Callbacks +--------------------- + +In certain scenarios, you may need to temporarily prevent certain callbacks from +being executed within your Rails application. This can be useful when you want +to skip specific actions during certain operations without permanently disabling +the callbacks. + +Rails provides a mechanism for suppressing callbacks using the +[`ActiveRecord::Suppressor` +module](https://api.rubyonrails.org/classes/ActiveRecord/Suppressor.html). By +using this module, you can wrap a block of code where you want to suppress +callbacks, ensuring that they are not executed during that specific operation. + +Let's consider a scenario where we have a `User` model with a callback that +sends a welcome email to new users after they sign up. However, there might be +cases where we want to create a user without sending the welcome email, such as +during seeding the database with test data. + +```ruby +class User < ApplicationRecord + after_create :send_welcome_email + + def send_welcome_email + puts "Welcome email sent to #{self.email}" + end +end +``` +In this example, the `after_create` callback triggers the `send_welcome_email` +method every time a new user is created. + +To create a user without sending the welcome email, we can use the +`ActiveRecord::Suppressor` module as follows: + +```ruby +User.suppress do + User.create(name: "Jane", email: "jane@example.com") +end +``` + +In the above code, the `User.suppress` block ensures that the +`send_welcome_email` callback is not executed during the creation of the "Jane" +user, allowing us to create the user without sending the welcome email. + +WARNING: Using the Active Record Suppressor, while potentially beneficial for +selectively controlling callback execution, can introduce complexity and +unexpected behavior. Suppressing callbacks can obscure the intended flow of your +application, leading to difficulties in understanding and maintaining the +codebase over time. Carefully consider the implications of suppressing +callbacks, ensuring thorough documentation and thoughtful testing to mitigate +risks of unintended side effects, performance issues, and test failures. + Halting Execution ----------------- From a276e19d8f2c6c6b4d5fffaacb264bc43442b2a2 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Tue, 7 May 2024 13:02:33 +0200 Subject: [PATCH 48/86] rubocop fixes --- guides/source/active_record_callbacks.md | 44 +++++++++++------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index c6f37bb8128f9..5ad0438a7eff5 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -182,27 +182,26 @@ class User < ApplicationRecord validate :location_information, on: :location_info private + def personal_information + errors.add(:base, "Name must be present") if first_name.blank? + errors.add(:base, "Age must be at least 18") if age && age < 18 + end - def personal_information - errors.add(:base, "Name must be present") if first_name.blank? - errors.add(:base, "Age must be at least 18") if age && age < 18 - end - - def contact_information - errors.add(:base, "Email must be present") if email.blank? - errors.add(:base, "Phone number must be present") if phone.blank? - end + def contact_information + errors.add(:base, "Email must be present") if email.blank? + errors.add(:base, "Phone number must be present") if phone.blank? + end - def location_information - errors.add(:base, "Address must be present") if address.blank? - errors.add(:base, "City must be present") if city.blank? - end + def location_information + errors.add(:base, "Address must be present") if address.blank? + errors.add(:base, "City must be present") if city.blank? + end end ``` Then, you can use this custom context to trigger the validations: -```ruby +```irb irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St") irb> user.valid?(:personal_info) # => false irb> user.valid?(:contact_info) # => true @@ -213,7 +212,7 @@ You can also use the custom contexts to trigger the validations on any method that supports callbacks. For example, you could use the custom context to trigger the validations on `save`: -```ruby +```irb irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St") irb> user.save(context: :personal_info) # => false irb> user.save(context: :contact_info) # => true @@ -731,12 +730,11 @@ class User < ApplicationRecord before_save :log_email_change private - - def log_email_change - if email_changed? - Rails.logger.info("Email changed from #{email_was} to #{email}") + def log_email_change + if email_changed? + Rails.logger.info("Email changed from #{email_was} to #{email}") + end end - end end ``` @@ -744,9 +742,9 @@ Now, suppose there's a scenario where you want to update the user's email address without triggering the `before_save` callback to log the email change. You can use the `update_columns` method for this purpose: -```ruby -user = User.find(1) -user.update_columns(email: 'new_email@example.com') +```irb +irb> user = User.find(1) +irb> user.update_columns(email: 'new_email@example.com') ``` The above will update the user's email address without triggering the From 7dddb533a18178e1c5554d5f95f85073d6d4a427 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Tue, 7 May 2024 17:10:50 +0200 Subject: [PATCH 49/86] empty lines around code blocks --- guides/source/active_record_callbacks.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 5ad0438a7eff5..b0b2fdfdcf986 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -387,6 +387,7 @@ User created with email: john.doe@example.com User welcome email sent to: john.doe@example.com => # ``` + ### Updating an Object * [`before_validation`][] @@ -802,6 +803,7 @@ class User < ApplicationRecord end end ``` + In this example, the `after_create` callback triggers the `send_welcome_email` method every time a new user is created. From 18f397d40db941feeda5a2570142b79eeb51ec51 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 15 May 2024 20:40:10 +0200 Subject: [PATCH 50/86] change the name of the class --- guides/source/active_record_callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index b0b2fdfdcf986..55d72376d3202 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -33,13 +33,13 @@ whenever an Active Record object is initialized, created, saved, updated, delete or loaded from the database. ```ruby -class Baby < ApplicationRecord +class BirthdayCake < ApplicationRecord after_create -> { Rails.logger.info("Congratulations, the callback has run!") } end ``` ```irb -irb> baby = Baby.create +irb> cake = BirthdayCake.create Congratulations, the callback has run! ``` From 69b66b06dad9bc316fd02a6dba2301fcf4e67f17 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 15 May 2024 20:42:37 +0200 Subject: [PATCH 51/86] change the name of the class --- guides/source/active_record_callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index b0b2fdfdcf986..55d72376d3202 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -33,13 +33,13 @@ whenever an Active Record object is initialized, created, saved, updated, delete or loaded from the database. ```ruby -class Baby < ApplicationRecord +class BirthdayCake < ApplicationRecord after_create -> { Rails.logger.info("Congratulations, the callback has run!") } end ``` ```irb -irb> baby = Baby.create +irb> cake = BirthdayCake.create Congratulations, the callback has run! ``` From 77b5e44e849d5e387e043764f771cf80f969f407 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 16 May 2024 17:56:34 +0200 Subject: [PATCH 52/86] column wrap --- guides/source/active_record_callbacks.md | 107 ++++++++++++++--------- 1 file changed, 64 insertions(+), 43 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 55d72376d3202..9d7ff192d7bbe 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -12,8 +12,8 @@ After reading this guide, you will know: * How to register, run, and skip callbacks that respond to these events. * How to create relational, association, conditional, and transactional callbacks. -* How to create objects that encapsulate common behavior for your - callbacks to be reused. +* How to create objects that encapsulate common behavior for your callbacks to + be reused. -------------------------------------------------------------------------------- @@ -26,11 +26,11 @@ destroyed](active_record_basics.html#crud-reading-and-writing-data). Active Record provides hooks into this object life cycle so that you can control your application and its data. -Callbacks allow you to trigger logic before or after a change to an -object's state. They are methods that get called at certain moments of an -object's life cycle. With callbacks it is possible to write code that will run -whenever an Active Record object is initialized, created, saved, updated, deleted, validated, -or loaded from the database. +Callbacks allow you to trigger logic before or after a change to an object's +state. They are methods that get called at certain moments of an object's life +cycle. With callbacks it is possible to write code that will run whenever an +Active Record object is initialized, created, saved, updated, deleted, +validated, or loaded from the database. ```ruby class BirthdayCake < ApplicationRecord @@ -262,8 +262,8 @@ order in which they will get called** during the respective operations: https://api.rubyonrails.org/classes/ActiveModel/Validations/Callbacks/ClassMethods.html#method-i-before_validation There are examples below that show how to use these callbacks. We've grouped -them by the operation they are associated with, and lastly show how they can -be used in combination. +them by the operation they are associated with, and lastly show how they can be +used in combination. #### Validation Callbacks @@ -272,7 +272,9 @@ the [`valid?`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-valid-3F) ( or its alias [`validate`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-validate)) -or [`invalid?`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-invalid-3F) method, or indirectly via `create`, `update`, or `save`. They are called before +or +[`invalid?`](https://api.rubyonrails.org/classes/ActiveModel/Validations.html#method-i-invalid-3F) +method, or indirectly via `create`, `update`, or `save`. They are called before and after the validation phase. ```ruby @@ -756,24 +758,42 @@ important business rules and application logic in callbacks that you do not want to bypass. Bypassing them without understanding the potential implications may lead to invalid data. -[`decrement!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-decrement-21 -[`decrement_counter`]: https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-decrement_counter -[`delete`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-delete -[`delete_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_all -[`delete_by`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_by -[`increment!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-increment-21 -[`increment_counter`]: https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-increment_counter -[`insert`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert -[`insert!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert-21 -[`insert_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all -[`insert_all!`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all-21 -[`touch_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-touch_all -[`update_column`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column -[`update_columns`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_columns -[`update_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_all -[`update_counters`]: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_counters -[`upsert`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert -[`upsert_all`]: https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all +[`decrement!`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-decrement-21 +[`decrement_counter`]: + https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-decrement_counter +[`delete`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-delete +[`delete_all`]: + https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_all +[`delete_by`]: + https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_by +[`increment!`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-increment-21 +[`increment_counter`]: + https://api.rubyonrails.org/classes/ActiveRecord/CounterCache/ClassMethods.html#method-i-increment_counter +[`insert`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert +[`insert!`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert-21 +[`insert_all`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all +[`insert_all!`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-insert_all-21 +[`touch_all`]: + https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-touch_all +[`update_column`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_column +[`update_columns`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-update_columns +[`update_all`]: + https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_all +[`update_counters`]: + https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-update_counters +[`upsert`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert +[`upsert_all`]: + https://api.rubyonrails.org/classes/ActiveRecord/Persistence/ClassMethods.html#method-i-upsert_all Suppressing Callbacks --------------------- @@ -1082,11 +1102,11 @@ class PictureFile < ApplicationRecord end ``` -If anything raises an exception after the -`after_destroy` callback is called and the transaction rolls back, then 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. +If anything raises an exception after the `after_destroy` callback is called and +the transaction rolls back, then 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. ```ruby PictureFile.transaction do @@ -1123,15 +1143,15 @@ callback in order to allow other callbacks to run.

`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. However, -during `after_commit` the data was already persisted to the database, and thus any -exception won't roll anything back anymore. Also note that the code executed -within `after_commit` or `after_rollback` callbacks is itself not enclosed within a -transaction.

In the context of a single transaction, if you interact -with multiple loaded objects that represent the same record in the database, -there's a crucial behavior in the `after_commit` and `after_rollback` callbacks -to note. These callbacks are triggered only for the first object of the specific -record that changes within the transaction. Other loaded objects, despite -representing the same database record, will not have their respective +during `after_commit` the data was already persisted to the database, and thus +any exception won't roll anything back anymore. Also note that the code executed +within `after_commit` or `after_rollback` callbacks is itself not enclosed +within a transaction.

In the context of a single transaction, if you +interact with multiple loaded objects that represent the same record in the +database, there's a crucial behavior in the `after_commit` and `after_rollback` +callbacks to note. These callbacks are triggered only for the first object of +the specific record that changes within the transaction. Other loaded objects, +despite representing the same database record, will not have their respective `after_commit` or `after_rollback` callbacks triggered. This nuanced behavior is particularly impactful in scenarios where you expect independent callback execution for each object associated with the same database record. It can @@ -1211,7 +1231,8 @@ User was saved to database ### Transactional Callback Ordering -By default (from Rails 7.1), transaction callbacks will run in the order they are defined. +By default (from Rails 7.1), transaction callbacks will run in the order they +are defined. ```ruby class User < ActiveRecord::Base From 5dfac9747ff785750fea2ae524e23d7efa6e1954 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 3 Jun 2024 15:48:05 +0200 Subject: [PATCH 53/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 9d7ff192d7bbe..3cc28cb229190 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -39,7 +39,7 @@ end ``` ```irb -irb> cake = BirthdayCake.create +irb> BirthdayCake.create Congratulations, the callback has run! ``` From fdf38948e4dd47271a6079c448a651cf99cbb6fb Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 3 Jun 2024 15:48:38 +0200 Subject: [PATCH 54/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 3cc28cb229190..4911abc96c952 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -43,8 +43,8 @@ irb> BirthdayCake.create Congratulations, the callback has run! ``` -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. +As you will see, there are many life cycle events and multiple options to hook +into these — either before, after, or even around them. Callback Registration --------------------- From c59b22247ed23e808aa290b81ff7fa7c30595a2f Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 3 Jun 2024 15:49:11 +0200 Subject: [PATCH 55/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 4911abc96c952..701cf314b501e 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -54,8 +54,8 @@ Implementation can be done in a multitude of ways like using ordinary methods, blocks and procs, or defining custom callback objects using classes or modules. Let's go through each of these implementation techniques. -You can implement the callbacks as a **macro-style method that calls an ordinary -method** for registration. +You can register the callbacks with a **macro-style class method that calls an ordinary +method** for implementation. ```ruby class User < ApplicationRecord From 3efc433ba4966887ab366270da893922525a6512 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 3 Jun 2024 15:50:00 +0200 Subject: [PATCH 56/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 701cf314b501e..f5703ebf59a8c 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -315,14 +315,14 @@ called before, after, and around the object is saved. ```ruby class User < ApplicationRecord - before_save :encrypt_password + before_save :hash_password around_save :log_saving after_save :update_cache private - def encrypt_password + def hash_password self.password_digest = BCrypt::Password.create(password) - Rails.logger.info("Password encrypted for user with email: #{email}") + Rails.logger.info("Password hashed for user with email: #{email}") end def log_saving From 3160784db9142041bb45f2f5b2e7f687a6cefd1b Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 3 Jun 2024 15:50:27 +0200 Subject: [PATCH 57/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index f5703ebf59a8c..66523816a0526 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -351,7 +351,7 @@ Update Cache #### Create Callbacks Create callbacks are triggered whenever the record is persisted (i.e. "saved") -to the underlying database **for the first time**, in other words, when we're +to the underlying database **for the first time** — in other words, when we're saving a new record, via the `create` or `save` methods. They are called before, after and around the object is created. From b3286d04b75f022e1a4bfdf90871751635940faf Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 3 Jun 2024 15:51:41 +0200 Subject: [PATCH 58/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 66523816a0526..21e2f43785d2d 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1273,7 +1273,7 @@ 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_commit` callback to +Here's an example of an `after_commit` callback class to deal with the cleanup 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 From e089dccfce2b087978d6de407cfb7ceadd9e6b4f Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 3 Jun 2024 15:53:29 +0200 Subject: [PATCH 59/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 21e2f43785d2d..09ecf18d1a437 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1130,7 +1130,7 @@ 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 live cycle event. Read +supply the `:on` option the callback will fire for every life cycle event. Read more about `:on` [here](#registering-callbacks-to-fire-on-lifecycle-events. WARNING. When a transaction completes, the `after_commit` or `after_rollback` From 18f6fc6d045f0dcf7b889d938b1073ce43c25819 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Mon, 3 Jun 2024 15:54:10 +0200 Subject: [PATCH 60/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 09ecf18d1a437..f4ce9aebd2162 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -114,7 +114,7 @@ class AddUsername end ``` -### Registering Callbacks to Fire on Lifecycle Events +### Registering Callbacks to Fire on Life Cycle Events Callbacks can also be registered to only fire on certain life cycle events, this can be done using the `:on` option and allows complete control over when and in From fa25bd6b49b09f4f0651b30799d9253689443fb2 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 09:27:32 +0200 Subject: [PATCH 61/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index f4ce9aebd2162..e1cc003222ecd 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1245,7 +1245,7 @@ However, in prior versions of Rails, when defining multiple transactional `after_` callbacks (`after_commit`, `after_rollback`, etc), the order in which the callbacks were run was reversed. -If for some reason, you'd still like them to run in reverse you can set the +If for some reason you'd still like them to run in reverse, you can set the [following configuration](configuring.html#config-active-record-run-after-transaction-callbacks-in-order-defined) to `false`. The callbacks will then run in the reverse order. From 14bc126993f66febf5489dff54047d29f45823b5 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 09:29:13 +0200 Subject: [PATCH 62/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index e1cc003222ecd..5954d20bb0f2e 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -154,7 +154,7 @@ left public, they can be called from outside of the model and violate the principle of object encapsulation. WARNING. Refrain from using methods like `update`, `save`, or any other methods -that cause side effects on the object within your callback functions.

+that cause side effects on the object within your callback methods.

For instance, avoid calling `update(attribute: "value")` inside a callback. This practice can modify the model's state and potentially lead to unforeseen side effects during commit.

Instead, you can assign values directly (e.g., From 2a9bd13a592ea0f9706218ec413ce7589550b4c0 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 09:32:11 +0200 Subject: [PATCH 63/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 5954d20bb0f2e..6adea68e8303f 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -392,6 +392,9 @@ User welcome email sent to: john.doe@example.com ### Updating an Object +Update callbacks are triggered whenever an **existing** record is persisted (i.e. "saved") +to the underlying database. They are called before, after and around the object is updated. + * [`before_validation`][] * [`after_validation`][] * [`before_save`][] From 9623174a39805331a3faf25948782b8951a5cad5 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 09:32:52 +0200 Subject: [PATCH 64/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 6adea68e8303f..56eb8d00912ea 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -503,6 +503,8 @@ Notification sent to admin about critical info update for: john.doe.new@example. ### Destroying an Object +Destroy callbacks are triggered whenever a record is destroyed, but ignored when a record is deleted. They are called before, after and around the object is destroyed. + * [`before_destroy`][] * [`around_destroy`][] * [`after_destroy`][] From 82b51f23e83a9334198174a05c2c8d1d0f6ab22e Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 09:33:28 +0200 Subject: [PATCH 65/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 56eb8d00912ea..76e5f5bddfe13 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1136,7 +1136,7 @@ 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 life cycle event. Read -more about `:on` [here](#registering-callbacks-to-fire-on-lifecycle-events. +more about `:on` [here](#registering-callbacks-to-fire-on-life-cycle-events). WARNING. When a transaction completes, the `after_commit` or `after_rollback` callbacks are called for all models created, updated, or destroyed within that From 40f106a7147e8b42f97013c8e70750b0ef753055 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 10:21:02 +0200 Subject: [PATCH 66/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 76e5f5bddfe13..e9bb936379293 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -881,7 +881,7 @@ NOTE: If an `ActiveRecord::RecordNotDestroyed` is raised within `after_destroy`, Relational Callbacks -------------------- -Callbacks work through model relationships, and can even be defined by them. +Callbacks work through model relationships. Life cycle events can cascade on associations and fire callbacks. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the From f7c897fb5f2676b64d9f06d90a7f87e600fc6696 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 10:22:41 +0200 Subject: [PATCH 67/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 1 + 1 file changed, 1 insertion(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index e9bb936379293..d7af9110858dc 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1198,6 +1198,7 @@ class User < ApplicationRecord private def log_user_saved_to_db + # This only gets called once Rails.logger.info("User was saved to database") end end From 227e64b860a80a461ae6eea8fed219a03f4b78f7 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 11:17:13 +0200 Subject: [PATCH 68/86] update halting execution --- guides/source/active_record_callbacks.md | 57 +++++++++++++++++++----- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index d7af9110858dc..fd3113e1241d0 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -861,22 +861,57 @@ execution. This queue will include all of your model's validations, the registered callbacks, and the database operation to be executed. The whole callback chain is wrapped in a transaction. If any callback raises an -exception, the execution chain gets halted and a **rollback** is issued. To -intentionally halt a chain use: +exception, the execution chain gets halted and a **rollback** is issued, and the +error will be re-raised. ```ruby -throw :abort +class Product < ActiveRecord::Base + before_validation do + raise "Price can't be negative" if total_price < 0 + end +end + +Product.create # raises "Price can't be negative" +``` + +This unexpectedly breaks code that does not expect methods like `create` and +`save` to raise exceptions. + +Instead, you should use `throw :abort` to intentionally halt the chain. If any +callback throws `:abort`, the process will be aborted and `create` will return +false. + +```ruby +class Product < ActiveRecord::Base + before_validation do + throw :abort if total_price < 0 + end +end + +Product.create # => false ``` -WARNING. Any exception that is not `ActiveRecord::Rollback` or -`ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback -chain is halted. Additionally, it 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`, +`ActiveRecord::RecordNotSaved` or `ActiveRecord::RecordInvalid` will be +re-raised by Rails after the callback chain is halted. -NOTE: If an `ActiveRecord::RecordNotDestroyed` is raised within `after_destroy`, -`before_destroy` or `around_destroy` callback, it will not be re-raised and the -`destroy` method will return `false`. +When `throw :abort` is called in destroy callbacks, `destroy` will return false: + +```ruby +class User < ActiveRecord::Base + before_destroy do + throw :abort if still_active? + end +end +User.first.destroy # => false +``` + +Additionally, it will raise a `ActiveRecord::RecordNotDestroyed` when calling +`destroy!`. + +```ruby +User.first.destroy! # => raises an ActiveRecord::RecordNotDestroyed +``` Relational Callbacks -------------------- From 89645329a3ad7d14b1c2c257b9b4369e362102c4 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 09:22:21 +0000 Subject: [PATCH 69/86] move Conditional Callbacks after Running Callbacks --- guides/source/active_record_callbacks.md | 178 +++++++++++------------ 1 file changed, 89 insertions(+), 89 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index fd3113e1241d0..6579a0516fe2c 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -705,6 +705,95 @@ NOTE: The `find_by_*` and `find_by_*!` methods are dynamic finders generated automatically for every attribute. Learn more about them in the [Dynamic finders section](active_record_querying.html#dynamic-finders). +Conditional Callbacks +--------------------- + +As with [validations](active_record_validations.html), 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. + +```ruby +class Order < ApplicationRecord + before_save :normalize_card_number, if: :paid_with_card? +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: + +```ruby +class Order < ApplicationRecord + before_save :normalize_card_number, + if: ->(order) { order.paid_with_card? } +end +``` + +Since the proc is evaluated in the context of the object, it is also possible to +write this as: + +```ruby +class Order < ApplicationRecord + before_save :normalize_card_number, if: -> { paid_with_card? } +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?, -> { untrusted_author? }] +end +``` + +### Using Both `:if` and `:unless` + +Callbacks can mix both `:if` and `:unless` in the same declaration: + +```ruby +class Comment < ApplicationRecord + before_save :filter_content, + if: -> { forum.parental_control? }, + unless: -> { author.trusted? } +end +``` + +The callback only runs when all the `:if` conditions and none of the `:unless` +conditions are evaluated to `true`. + Skipping Callbacks ------------------ @@ -1026,95 +1115,6 @@ author.books = [book, book2] book.update(author_id: 1) ``` -Conditional Callbacks ---------------------- - -As with [validations](active_record_validations.html), 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. - -```ruby -class Order < ApplicationRecord - before_save :normalize_card_number, if: :paid_with_card? -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: - -```ruby -class Order < ApplicationRecord - before_save :normalize_card_number, - if: ->(order) { order.paid_with_card? } -end -``` - -Since the proc is evaluated in the context of the object, it is also possible to -write this as: - -```ruby -class Order < ApplicationRecord - before_save :normalize_card_number, if: -> { paid_with_card? } -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?, -> { untrusted_author? }] -end -``` - -### Using Both `:if` and `:unless` - -Callbacks can mix both `:if` and `:unless` in the same declaration: - -```ruby -class Comment < ApplicationRecord - before_save :filter_content, - if: -> { forum.parental_control? }, - unless: -> { author.trusted? } -end -``` - -The callback only runs when all the `:if` conditions and none of the `:unless` -conditions are evaluated to `true`. - Transaction Callbacks --------------------- From 105708e58f415cf50b78e17b1aaade58eb7f6af3 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 17:26:22 +0200 Subject: [PATCH 70/86] after_save commit --- guides/source/active_record_callbacks.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 6579a0516fe2c..7705db20880c4 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1220,11 +1220,10 @@ class PictureFile < ApplicationRecord end ``` -WARNING. Using both `after_create_commit` and `after_update_commit` with the -same method name will only allow the last callback defined to take effect, as -they both internally alias to `after_commit` which overrides previously defined -callbacks with the same method name. In this case, it's better to use -`after_save_commit` instead. +Using both `after_create_commit` and `after_update_commit` with the same method +name will only allow the last callback defined to take effect, as they both +internally alias to `after_commit` which overrides previously defined callbacks +with the same method name. ```ruby class User < ApplicationRecord @@ -1246,6 +1245,19 @@ irb> user.save # updating @user User was saved to database ``` +In this case, it's better to use `after_save_commit` instead. + +```ruby +class User < ApplicationRecord + after_save_commit :log_user_saved_to_db + + private + def log_user_saved_to_db + Rails.logger.info("User was saved to database") + end +end +``` + ### `after_save_commit` There is also [`after_save_commit`][], which is an alias for using the From d27a25a657b90c623a714c22a8855e991170129b Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 17:33:53 +0200 Subject: [PATCH 71/86] after_destroy_commit --- guides/source/active_record_callbacks.md | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 7705db20880c4..ea2fce04801ef 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1204,9 +1204,12 @@ transaction.

Since using the `after_commit` callback only on create, update, or delete is common, there are aliases for those operations: +* [`after_destroy_commit`][] * [`after_create_commit`][] * [`after_update_commit`][] -* [`after_destroy_commit`][] + + +You can use the `after_destroy_commit` as follows: ```ruby class PictureFile < ApplicationRecord @@ -1220,8 +1223,8 @@ class PictureFile < ApplicationRecord end ``` -Using both `after_create_commit` and `after_update_commit` with the same method -name will only allow the last callback defined to take effect, as they both +If you use the `after_create_commit` and the `after_update_commit` callback with the same method +name, it will only allow the last callback defined to take effect, as they both internally alias to `after_commit` which overrides previously defined callbacks with the same method name. @@ -1247,20 +1250,9 @@ User was saved to database In this case, it's better to use `after_save_commit` instead. -```ruby -class User < ApplicationRecord - after_save_commit :log_user_saved_to_db - - private - def log_user_saved_to_db - Rails.logger.info("User was saved to database") - end -end -``` - ### `after_save_commit` -There is also [`after_save_commit`][], which is an alias for using the +There is an [`after_save_commit`][], which is an alias for using the `after_commit` callback for both create and update together: ```ruby From 82308d8be2836d3bc426c568b16a7ea10a974b43 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 18:32:11 +0200 Subject: [PATCH 72/86] after_commit aliases --- guides/source/active_record_callbacks.md | 34 +++++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index ea2fce04801ef..b8273ea70b5a8 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1201,15 +1201,31 @@ transaction.

### Aliases for `after_commit` -Since using the `after_commit` callback only on create, update, or delete is -common, there are aliases for those operations: +Using the `after_commit` callback only on create, update, or delete is +common. Sometimes you may also want to use a single callback for both `create` and `update`. Here are some common aliases for these operations: * [`after_destroy_commit`][] * [`after_create_commit`][] * [`after_update_commit`][] +* [`after_save_commit`][] +Let's go through some examples: -You can use the `after_destroy_commit` as follows: +Instead of using `after_commit` with the `on` option for a destroy like below: + +```ruby +class PictureFile < ApplicationRecord + after_commit :delete_picture_file_from_disk, on: :destroy + + def delete_picture_file_from_disk + if File.exist?(filepath) + File.delete(filepath) + end + end +end +``` + +You can instead use the `after_destroy_commit`. ```ruby class PictureFile < ApplicationRecord @@ -1223,7 +1239,9 @@ class PictureFile < ApplicationRecord end ``` -If you use the `after_create_commit` and the `after_update_commit` callback with the same method +The same applies for `after_create_commit` and `after_update_commit`. + +However, if you use the `after_create_commit` and the `after_update_commit` callback with the same method name, it will only allow the last callback defined to take effect, as they both internally alias to `after_commit` which overrides previously defined callbacks with the same method name. @@ -1248,12 +1266,8 @@ irb> user.save # updating @user User was saved to database ``` -In this case, it's better to use `after_save_commit` instead. - -### `after_save_commit` - -There is an [`after_save_commit`][], which is an alias for using the -`after_commit` callback for both create and update together: +In this case, it's better to use `after_save_commit` instead which is an alias for using the +`after_commit` callback for both create and update: ```ruby class User < ApplicationRecord From 945400a9d05ae99226e0470dada29dfbe4e7cdb0 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Wed, 5 Jun 2024 18:57:41 +0200 Subject: [PATCH 73/86] after_commit and after_rollback --- guides/source/active_record_callbacks.md | 72 ++++++++++++++++++++---- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index b8273ea70b5a8..868c45bfbf12c 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1173,31 +1173,81 @@ 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 life cycle event. Read more about `:on` [here](#registering-callbacks-to-fire-on-life-cycle-events). -WARNING. When a transaction completes, the `after_commit` or `after_rollback` +When a transaction completes, the `after_commit` or `after_rollback` callbacks are called for all models created, updated, or destroyed within that transaction. However, if an exception is raised within one of these callbacks, the exception will bubble up and any remaining `after_commit` or -`after_rollback` methods will _not_ be executed. As such, if your callback code -could raise an exception, you'll need to rescue it and handle it within the -callback in order to allow other callbacks to run.

`after_commit` makes +`after_rollback` methods will _not_ be executed. + +```ruby +class User < ActiveRecord::Base + after_commit { raise } + after_commit { # this won't get called } +end +``` + +WARNING. If your callback code raises an exception, you'll need to rescue it and handle it within the +callback in order to allow other callbacks to run. + +`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. However, -during `after_commit` the data was already persisted to the database, and thus -any exception won't roll anything back anymore. Also note that the code executed +transaction will be rolled back and the data will not be persisted. + +```ruby +class User < ActiveRecord::Base + after_save do + # If this fails the user won't be saved. + EventLog.create!(event: "user_saved") + end +end +``` + +However, during `after_commit` the data was already persisted to the database, and thus +any exception won't roll anything back anymore. + +```ruby +class User < ActiveRecord::Base + after_commit do + # If this fails the user was already saved. + EventLog.create!(event: "user_saved") + end +end +``` + +The code executed within `after_commit` or `after_rollback` callbacks is itself not enclosed -within a transaction.

In the context of a single transaction, if you -interact with multiple loaded objects that represent the same record in the +within a transaction. + +In the context of a single transaction, if you +represent the same record in the database, there's a crucial behavior in the `after_commit` and `after_rollback` callbacks to note. These callbacks are triggered only for the first object of the specific record that changes within the transaction. Other loaded objects, despite representing the same database record, will not have their respective -`after_commit` or `after_rollback` callbacks triggered. This nuanced behavior is +`after_commit` or `after_rollback` callbacks triggered. + +```ruby +class User < ApplicationRecord + after_commit :log_user_saved_to_db, on: :update + private + def log_user_saved_to_db + Rails.logger.info("User was saved to database") + end +end +``` + +```ruby +irb> user = User.create +irb> User.transaction { user.save; user.save } +# User was saved to database +``` +WARNING: This nuanced behavior is particularly impactful in scenarios where you expect independent callback execution for each object associated with the same database record. It can influence the flow and predictability of callback sequences, leading to potential inconsistencies in application logic following the -transaction.

+transaction. ### Aliases for `after_commit` From b19857536cb4e2859f1179c03bac9ef34034d187 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 6 Jun 2024 10:00:39 +0200 Subject: [PATCH 74/86] halting and execution --- guides/source/active_record_callbacks.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 868c45bfbf12c..14067282083b9 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -980,11 +980,12 @@ end Product.create # => false ``` -WARNING: Any exception that is not `ActiveRecord::Rollback`, -`ActiveRecord::RecordNotSaved` or `ActiveRecord::RecordInvalid` will be -re-raised by Rails after the callback chain is halted. +WARNING: If an exception occurs during the callback chain, Rails will re-raise +it unless it is an `ActiveRecord::Rollback`, `ActiveRecord::RecordNotSaved` or +`ActiveRecord::RecordInvalid` exception. -When `throw :abort` is called in destroy callbacks, `destroy` will return false: + +When `throw :abort` is called in any destroy callback, `destroy` will return false: ```ruby class User < ActiveRecord::Base @@ -992,16 +993,19 @@ class User < ActiveRecord::Base throw :abort if still_active? end end + User.first.destroy # => false ``` -Additionally, it will raise a `ActiveRecord::RecordNotDestroyed` when calling +However, it will raise an `ActiveRecord::RecordNotDestroyed` when calling `destroy!`. ```ruby User.first.destroy! # => raises an ActiveRecord::RecordNotDestroyed ``` +In addition to the behaviors mentioned, it's important to note that when `throw :abort` is called in a `before_* callback` (such as `before_save`, `before_create`, or `before_update`), it will raise an `ActiveRecord::RecordNotSaved` exception. This exception indicates that the record was not saved due to the callback's interruption. Therefore, when using `throw :abort` in `before_*` callbacks, you should be prepared to handle the `ActiveRecord::RecordNotSaved` exception. + Relational Callbacks -------------------- From ca9f4fbef825a6e093fca3c5751b44b264b9e780 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 6 Jun 2024 10:01:53 +0200 Subject: [PATCH 75/86] remove custom context section --- guides/source/active_record_callbacks.md | 61 +----------------------- 1 file changed, 1 insertion(+), 60 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 14067282083b9..1e922286b9843 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -124,8 +124,7 @@ NOTE: A context is like a category or a scenario in which you want certain validations to apply. When you validate an ActiveRecord model, you can specify a context to group validations. This allows you to have different sets of validations that apply in different situations. In Rails, there are certain -default contexts for validations like :create, :update, and :save. However, you -can define [custom contexts](#custom-contexts) as well. +default contexts for validations like :create, :update, and :save. ```ruby class User < ApplicationRecord @@ -161,64 +160,6 @@ effects during commit.

Instead, you can assign values directly (e.g., `self.attribute = "value"`) in `before_create`, `before_update`, or earlier callbacks for a safer approach. -#### Custom Contexts - -You can define your own custom contexts for callbacks. This can be useful when -you want to perform validations based on specific scenarios, or you want to -group certain callbacks together and run them in a specific context. In these -cases you may be tempted to [skip callbacks](#skipping-callbacks) altogether, -but defining a custom context can sometimes be an alternative structured -approach. You will need to combine a `context` with the `on` option to define a -custom context for a callback. - -A common scenario for custom contexts is when you have a multi-step form where -you want to perform validations per step. You can define custom context for each -step of the form: - -```ruby -class User < ApplicationRecord - validate :personal_information, on: :personal_info - validate :contact_information, on: :contact_info - validate :location_information, on: :location_info - - private - def personal_information - errors.add(:base, "Name must be present") if first_name.blank? - errors.add(:base, "Age must be at least 18") if age && age < 18 - end - - def contact_information - errors.add(:base, "Email must be present") if email.blank? - errors.add(:base, "Phone number must be present") if phone.blank? - end - - def location_information - errors.add(:base, "Address must be present") if address.blank? - errors.add(:base, "City must be present") if city.blank? - end -end -``` - -Then, you can use this custom context to trigger the validations: - -```irb -irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St") -irb> user.valid?(:personal_info) # => false -irb> user.valid?(:contact_info) # => true -irb> user.valid?(:location_info) # => false -``` - -You can also use the custom contexts to trigger the validations on any method -that supports callbacks. For example, you could use the custom context to -trigger the validations on `save`: - -```irb -irb> user = User.new(name: "John Doe", age: 17, email: "jane@example.com", phone: "1234567890", address: "123 Main St") -irb> user.save(context: :personal_info) # => false -irb> user.save(context: :contact_info) # => true -irb> user.save(context: :location_info) # => false -``` - Available Callbacks ------------------- From 2f31af9dd54e5f0ac6280834d086c027aca62ad8 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 6 Jun 2024 10:39:35 +0200 Subject: [PATCH 76/86] association callbacks --- guides/source/active_record_callbacks.md | 73 ++++++++++++------------ 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 1e922286b9843..4aa9ab9369495 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -947,42 +947,6 @@ User.first.destroy! # => raises an ActiveRecord::RecordNotDestroyed In addition to the behaviors mentioned, it's important to note that when `throw :abort` is called in a `before_* callback` (such as `before_save`, `before_create`, or `before_update`), it will raise an `ActiveRecord::RecordNotSaved` exception. This exception indicates that the record was not saved due to the callback's interruption. Therefore, when using `throw :abort` in `before_*` callbacks, you should be prepared to handle the `ActiveRecord::RecordNotSaved` exception. -Relational Callbacks --------------------- - -Callbacks work through model relationships. Life cycle events can cascade on associations and fire callbacks. - -Suppose an example where a user has many articles. A user's articles should be -destroyed if the user is destroyed. Let's add an `after_destroy` callback to the -`User` model by way of its relationship to the `Article` model: - -```ruby -class User < ApplicationRecord - has_many :articles, dependent: :destroy -end - -class Article < ApplicationRecord - after_destroy :log_destroy_action - - def log_destroy_action - Rails.logger.info("Article destroyed") - end -end -``` - -```irb -irb> user = User.first -=> # -irb> user.articles.create! -=> #
-irb> user.destroy -Article destroyed -=> # -``` - -WARNING: When using a `before_destroy` callback, it 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`. Association Callbacks --------------------- @@ -1060,6 +1024,43 @@ author.books = [book, book2] book.update(author_id: 1) ``` +Cascading Association Callbacks +-------------------- + +Callbacks can be performed when asssociated objects are changed. They work through the model associations whereby life cycle events can cascade on associations and fire callbacks. + +Suppose an example where a user has many articles. A user's articles should be +destroyed if the user is destroyed. Let's add an `after_destroy` callback to the +`User` model by way of its association to the `Article` model: + +```ruby +class User < ApplicationRecord + has_many :articles, dependent: :destroy +end + +class Article < ApplicationRecord + after_destroy :log_destroy_action + + def log_destroy_action + Rails.logger.info("Article destroyed") + end +end +``` + +```irb +irb> user = User.first +=> # +irb> user.articles.create! +=> #
+irb> user.destroy +Article destroyed +=> # +``` + +WARNING: When using a `before_destroy` callback, it 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`. + Transaction Callbacks --------------------- From 3499dc8acf795f82895a85621b7e97a83744aaad Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 6 Jun 2024 10:41:05 +0200 Subject: [PATCH 77/86] column wrap --- guides/source/active_record_callbacks.md | 94 +++++++++++++----------- 1 file changed, 52 insertions(+), 42 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 4aa9ab9369495..ad163d0b512a0 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -54,8 +54,8 @@ Implementation can be done in a multitude of ways like using ordinary methods, blocks and procs, or defining custom callback objects using classes or modules. Let's go through each of these implementation techniques. -You can register the callbacks with a **macro-style class method that calls an ordinary -method** for implementation. +You can register the callbacks with a **macro-style class method that calls an +ordinary method** for implementation. ```ruby class User < ApplicationRecord @@ -333,8 +333,9 @@ User welcome email sent to: john.doe@example.com ### Updating an Object -Update callbacks are triggered whenever an **existing** record is persisted (i.e. "saved") -to the underlying database. They are called before, after and around the object is updated. +Update callbacks are triggered whenever an **existing** record is persisted +(i.e. "saved") to the underlying database. They are called before, after and +around the object is updated. * [`before_validation`][] * [`after_validation`][] @@ -444,7 +445,9 @@ Notification sent to admin about critical info update for: john.doe.new@example. ### Destroying an Object -Destroy callbacks are triggered whenever a record is destroyed, but ignored when a record is deleted. They are called before, after and around the object is destroyed. +Destroy callbacks are triggered whenever a record is destroyed, but ignored when +a record is deleted. They are called before, after and around the object is +destroyed. * [`before_destroy`][] * [`around_destroy`][] @@ -926,7 +929,8 @@ it unless it is an `ActiveRecord::Rollback`, `ActiveRecord::RecordNotSaved` or `ActiveRecord::RecordInvalid` exception. -When `throw :abort` is called in any destroy callback, `destroy` will return false: +When `throw :abort` is called in any destroy callback, `destroy` will return +false: ```ruby class User < ActiveRecord::Base @@ -945,7 +949,13 @@ However, it will raise an `ActiveRecord::RecordNotDestroyed` when calling User.first.destroy! # => raises an ActiveRecord::RecordNotDestroyed ``` -In addition to the behaviors mentioned, it's important to note that when `throw :abort` is called in a `before_* callback` (such as `before_save`, `before_create`, or `before_update`), it will raise an `ActiveRecord::RecordNotSaved` exception. This exception indicates that the record was not saved due to the callback's interruption. Therefore, when using `throw :abort` in `before_*` callbacks, you should be prepared to handle the `ActiveRecord::RecordNotSaved` exception. +In addition to the behaviors mentioned, it's important to note that when `throw +:abort` is called in a `before_* callback` (such as `before_save`, +`before_create`, or `before_update`), it will raise an +`ActiveRecord::RecordNotSaved` exception. This exception indicates that the +record was not saved due to the callback's interruption. Therefore, when using +`throw :abort` in `before_*` callbacks, you should be prepared to handle the +`ActiveRecord::RecordNotSaved` exception. Association Callbacks @@ -1027,7 +1037,9 @@ book.update(author_id: 1) Cascading Association Callbacks -------------------- -Callbacks can be performed when asssociated objects are changed. They work through the model associations whereby life cycle events can cascade on associations and fire callbacks. +Callbacks can be performed when asssociated objects are changed. They work +through the model associations whereby life cycle events can cascade on +associations and fire callbacks. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the @@ -1119,8 +1131,8 @@ 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 life cycle event. Read more about `:on` [here](#registering-callbacks-to-fire-on-life-cycle-events). -When a transaction completes, the `after_commit` or `after_rollback` -callbacks are called for all models created, updated, or destroyed within that +When a transaction completes, the `after_commit` or `after_rollback` callbacks +are called for all models created, updated, or destroyed within that transaction. However, if an exception is raised within one of these callbacks, the exception will bubble up and any remaining `after_commit` or `after_rollback` methods will _not_ be executed. @@ -1132,13 +1144,13 @@ class User < ActiveRecord::Base end ``` -WARNING. If your callback code raises an exception, you'll need to rescue it and handle it within the -callback in order to allow other callbacks to run. +WARNING. If your callback code raises an exception, you'll need to rescue it and +handle it within the callback in order to allow other callbacks to run. -`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. +`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. ```ruby class User < ActiveRecord::Base @@ -1149,8 +1161,8 @@ class User < ActiveRecord::Base end ``` -However, during `after_commit` the data was already persisted to the database, and thus -any exception won't roll anything back anymore. +However, during `after_commit` the data was already persisted to the database, +and thus any exception won't roll anything back anymore. ```ruby class User < ActiveRecord::Base @@ -1161,12 +1173,10 @@ class User < ActiveRecord::Base end ``` -The code executed -within `after_commit` or `after_rollback` callbacks is itself not enclosed -within a transaction. +The code executed within `after_commit` or `after_rollback` callbacks is itself +not enclosed within a transaction. -In the context of a single transaction, if you -represent the same record in the +In the context of a single transaction, if you represent the same record in the database, there's a crucial behavior in the `after_commit` and `after_rollback` callbacks to note. These callbacks are triggered only for the first object of the specific record that changes within the transaction. Other loaded objects, @@ -1188,17 +1198,17 @@ irb> user = User.create irb> User.transaction { user.save; user.save } # User was saved to database ``` -WARNING: This nuanced behavior is -particularly impactful in scenarios where you expect independent callback -execution for each object associated with the same database record. It can -influence the flow and predictability of callback sequences, leading to -potential inconsistencies in application logic following the -transaction. +WARNING: This nuanced behavior is particularly impactful in scenarios where you +expect independent callback execution for each object associated with the same +database record. It can influence the flow and predictability of callback +sequences, leading to potential inconsistencies in application logic following +the transaction. ### Aliases for `after_commit` -Using the `after_commit` callback only on create, update, or delete is -common. Sometimes you may also want to use a single callback for both `create` and `update`. Here are some common aliases for these operations: +Using the `after_commit` callback only on create, update, or delete is common. +Sometimes you may also want to use a single callback for both `create` and +`update`. Here are some common aliases for these operations: * [`after_destroy_commit`][] * [`after_create_commit`][] @@ -1237,10 +1247,10 @@ end The same applies for `after_create_commit` and `after_update_commit`. -However, if you use the `after_create_commit` and the `after_update_commit` callback with the same method -name, it will only allow the last callback defined to take effect, as they both -internally alias to `after_commit` which overrides previously defined callbacks -with the same method name. +However, if you use the `after_create_commit` and the `after_update_commit` +callback with the same method name, it will only allow the last callback defined +to take effect, as they both internally alias to `after_commit` which overrides +previously defined callbacks with the same method name. ```ruby class User < ApplicationRecord @@ -1262,8 +1272,8 @@ irb> user.save # updating @user User was saved to database ``` -In this case, it's better to use `after_save_commit` instead which is an alias for using the -`after_commit` callback for both create and update: +In this case, it's better to use `after_save_commit` instead which is an alias +for using the `after_commit` callback for both create and update: ```ruby class User < ApplicationRecord @@ -1328,11 +1338,11 @@ 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 of an `after_commit` callback class to -deal with the cleanup 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. +Here's an example of an `after_commit` callback class to deal with the cleanup +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 FileDestroyerCallback From 02eabdedaadd848a7f5e2abf3198ca07d63e01a5 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 6 Jun 2024 10:42:34 +0200 Subject: [PATCH 78/86] lint --- guides/source/active_record_callbacks.md | 1 + 1 file changed, 1 insertion(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index ad163d0b512a0..e667fb658afa6 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1198,6 +1198,7 @@ irb> user = User.create irb> User.transaction { user.save; user.save } # User was saved to database ``` + WARNING: This nuanced behavior is particularly impactful in scenarios where you expect independent callback execution for each object associated with the same database record. It can influence the flow and predictability of callback From c51774a94e0ea7f574a9fecb6b3e793c6cb53e0a Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 6 Jun 2024 20:51:11 +0200 Subject: [PATCH 79/86] fix rubocop errors --- guides/source/active_record_callbacks.md | 28 ++++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index e667fb658afa6..d17f959e29da1 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -899,9 +899,9 @@ error will be re-raised. ```ruby class Product < ActiveRecord::Base - before_validation do - raise "Price can't be negative" if total_price < 0 - end + before_validation do + raise "Price can't be negative" if total_price < 0 + end end Product.create # raises "Price can't be negative" @@ -916,9 +916,9 @@ false. ```ruby class Product < ActiveRecord::Base - before_validation do - throw :abort if total_price < 0 - end + before_validation do + throw :abort if total_price < 0 + end end Product.create # => false @@ -934,9 +934,9 @@ false: ```ruby class User < ActiveRecord::Base - before_destroy do - throw :abort if still_active? - end + before_destroy do + throw :abort if still_active? + end end User.first.destroy # => false @@ -1139,8 +1139,11 @@ the exception will bubble up and any remaining `after_commit` or ```ruby class User < ActiveRecord::Base - after_commit { raise } - after_commit { # this won't get called } + after_commit { raise "Intentional Error" } + after_commit { + # This won't get called because the previous after_commit raises an exception + Rails.logger.info("This will not be logged") + } end ``` @@ -1186,6 +1189,7 @@ despite representing the same database record, will not have their respective ```ruby class User < ApplicationRecord after_commit :log_user_saved_to_db, on: :update + private def log_user_saved_to_db Rails.logger.info("User was saved to database") @@ -1193,7 +1197,7 @@ class User < ApplicationRecord end ``` -```ruby +```irb irb> user = User.create irb> User.transaction { user.save; user.save } # User was saved to database From 7fcd04c64d0f0149f1e9a06f121a821d01745ff3 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 6 Jun 2024 20:56:29 +0200 Subject: [PATCH 80/86] fix rubocop errors --- guides/source/active_record_callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index d17f959e29da1..30d5f74ca79b0 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -935,7 +935,7 @@ false: ```ruby class User < ActiveRecord::Base before_destroy do - throw :abort if still_active? + throw :abort if still_active? end end @@ -1192,7 +1192,7 @@ class User < ApplicationRecord private def log_user_saved_to_db - Rails.logger.info("User was saved to database") + Rails.logger.info("User was saved to database") end end ``` From 87f200f4135ed075068850fab9296758932e223b Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Thu, 6 Jun 2024 22:19:42 +0200 Subject: [PATCH 81/86] Trigger Build From 76113ec5d06627c87ed450f7b1160252be3183f8 Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 7 Jun 2024 12:45:29 +0200 Subject: [PATCH 82/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 30d5f74ca79b0..2505382f8cdcf 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -1035,7 +1035,7 @@ book.update(author_id: 1) ``` Cascading Association Callbacks --------------------- +------------------------------- Callbacks can be performed when asssociated objects are changed. They work through the model associations whereby life cycle events can cascade on From 5f64bc3fb28f46337d8e14285d68831fa649775a Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 7 Jun 2024 12:46:38 +0200 Subject: [PATCH 83/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 2505382f8cdcf..2f3f2aed37a05 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -924,9 +924,13 @@ end Product.create # => false ``` -WARNING: If an exception occurs during the callback chain, Rails will re-raise -it unless it is an `ActiveRecord::Rollback`, `ActiveRecord::RecordNotSaved` or -`ActiveRecord::RecordInvalid` exception. +However, it will raise an `ActiveRecord::RecordNotSaved` when calling +`create!`. This exception indicates that the record was not saved due to +the callback's interruption. + +```ruby +User.create! # => raises an ActiveRecord::RecordNotSaved +``` When `throw :abort` is called in any destroy callback, `destroy` will return From b0d046796e83be36b23595f6e20a1526ea55647b Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 7 Jun 2024 12:51:52 +0200 Subject: [PATCH 84/86] Update guides/source/active_record_callbacks.md Co-authored-by: Petrik de Heus --- guides/source/active_record_callbacks.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 2f3f2aed37a05..c67597cdee406 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -910,6 +910,8 @@ Product.create # raises "Price can't be negative" This unexpectedly breaks code that does not expect methods like `create` and `save` to raise exceptions. +NOTE: If an exception occurs during the callback chain, Rails will re-raise +it unless it is an `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` exception. Instead, you should use `throw :abort` to intentionally halt the chain. If any callback throws `:abort`, the process will be aborted and `create` will return false. From 1a0e6ba68a4c3f0f7d6f0710b4edf8a36818697a Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 7 Jun 2024 12:53:30 +0200 Subject: [PATCH 85/86] remove section from Halting Callbacks --- guides/source/active_record_callbacks.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index c67597cdee406..528be9187c59e 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -955,15 +955,6 @@ However, it will raise an `ActiveRecord::RecordNotDestroyed` when calling User.first.destroy! # => raises an ActiveRecord::RecordNotDestroyed ``` -In addition to the behaviors mentioned, it's important to note that when `throw -:abort` is called in a `before_* callback` (such as `before_save`, -`before_create`, or `before_update`), it will raise an -`ActiveRecord::RecordNotSaved` exception. This exception indicates that the -record was not saved due to the callback's interruption. Therefore, when using -`throw :abort` in `before_*` callbacks, you should be prepared to handle the -`ActiveRecord::RecordNotSaved` exception. - - Association Callbacks --------------------- From 59220f89d7e0e7ae739cb654f15e2c76500773fb Mon Sep 17 00:00:00 2001 From: Ridhwana Date: Fri, 7 Jun 2024 12:54:23 +0200 Subject: [PATCH 86/86] column wrap --- guides/source/active_record_callbacks.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index 528be9187c59e..bfa6712facca3 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -910,11 +910,11 @@ Product.create # raises "Price can't be negative" This unexpectedly breaks code that does not expect methods like `create` and `save` to raise exceptions. -NOTE: If an exception occurs during the callback chain, Rails will re-raise -it unless it is an `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` exception. -Instead, you should use `throw :abort` to intentionally halt the chain. If any -callback throws `:abort`, the process will be aborted and `create` will return -false. +NOTE: If an exception occurs during the callback chain, Rails will re-raise it +unless it is an `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` +exception. Instead, you should use `throw :abort` to intentionally halt the +chain. If any callback throws `:abort`, the process will be aborted and `create` +will return false. ```ruby class Product < ActiveRecord::Base @@ -926,9 +926,9 @@ end Product.create # => false ``` -However, it will raise an `ActiveRecord::RecordNotSaved` when calling -`create!`. This exception indicates that the record was not saved due to -the callback's interruption. +However, it will raise an `ActiveRecord::RecordNotSaved` when calling `create!`. +This exception indicates that the record was not saved due to the callback's +interruption. ```ruby User.create! # => raises an ActiveRecord::RecordNotSaved