回调允许你在对象状态发生变化之前或之后触发逻辑,它们是在对象生命周期某个确定时刻调用的方法。
代码感受一下
ruby
class BirthdayCake < ApplicationRecord
after_create -> { Rails.logger.info("Congratulations, the callback has run!") }
end
ruby
irb> BirthdayCake.create
Congratulations, the callback has run!
1 Callback Registration
1.1 当普通方法调用
ruby
class User < ApplicationRecord
validates :username, :email, presence: true
before_validation :ensure_username_has_value
private
def ensure_username_has_value
if username.blank?
self.username = email
end
end
end
1.2 当一个代码块调用。如果代码块内的代码很短,只需一行,可以考虑这种方式
ruby
class User < ApplicationRecord
validates :username, :email, presence: true
before_validation do
self.username = email if username.blank?
end
end
1.3 也可以将一个 proc 传递给要触发的回调。
ruby
class User < ApplicationRecord
validates :username, :email, presence: true
before_validation ->(user) { user.username = user.email if user.username.blank? }
end
1.4 也可以定义一个自定义回调对象。
ruby
class User < ApplicationRecord
validates :username, :email, presence: true
before_validation AddUsername
end
class AddUsername
def self.before_validation(record)
if record.username.blank?
record.username = record.email
end
end
end
1.5 回调也可以注册为仅在特定生命周期事件中触发,可以使用 :on 选项来完成,并允许完全控制何时以及在何种情况下触发回调。
ruby
class User < ApplicationRecord
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 ensure_username_has_value
if username.blank?
self.username = email
end
end
def set_location
self.location = LocationService.query(self)
end
end
2. Available Callbacks
2.1 Validation Callbacks.
before_validation/after_validation, 当记录直接通过valid?(或者别名validate)或invalid?方法验证时或者直接通过create, update ,save, 验证回调会被触发。
ruby
class User < ApplicationRecord
validates :name, presence: true
before_validation :titleize_name
after_validation :log_errors
private
def titleize_name
self.name = name.downcase.titleize if name.present?
Rails.logger.info("Name titleized to #{name}")
end
def log_errors
if errors.any?
Rails.logger.error("Validation failed: #{errors.full_messages.join(', ')}")
end
end
end
ruby
irb> user = User.new(name: "", email: "john.doe@example.com", password: "abc123456")
=> #<User id: nil, email: "john.doe@example.com", created_at: nil, updated_at: nil, name: "">
irb> user.valid?
Name titleized to
Validation failed: Name can't be blank
=> false
2.2 Save Callbacks
ruby
class User < ApplicationRecord
before_save :hash_password
around_save :log_saving
after_save :update_cache
private
def hash_password
self.password_digest = BCrypt::Password.create(password)
Rails.logger.info("Password hashed 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", self], attributes)
Rails.logger.info("Update Cache")
end
end
ruby
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
User saved with email: jane.doe@example.com
Update Cache
=> #<User id: 1, email: "jane.doe@example.com", created_at: "2024-03-20 16:02:43.685500000 +0000", updated_at: "2024-03-20 16:02:43.685500000 +0000", name: "Jane Doe">
2.3 Create Callbacks
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
ruby
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
=> #<User id: 10, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe">
2.4 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
ruby
irb> user = User.find(1)
=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "user" >
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
2.5 Destroy Callbacks
ruby
class User < ApplicationRecord
before_destroy :check_admin_count
around_destroy :log_destroy_operation
after_destroy :notify_users
private
def check_admin_count
if admin? && User.where(role: "admin").count == 1
throw :abort
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
UserMailer.deletion_email(self).deliver_later
Rails.logger.info("Notification sent to other users about user deletion")
end
end
ruby
irb> user = User.find(1)
=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "admin">
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
2.6 after_initialize and after_find
每当实例化Active Record对象时,无论是直接使用new还是从数据库加载记录,都会调用after_initialize回调,这对于避免直接覆盖Active Record 初始化方法非常有效.
如果after_initialize和after_find都定义了,after_find会先调用。
ruby
class User < ApplicationRecord
after_initialize do |user|
Rails.logger.info("You have initialized an object!")
end
after_find do |user|
Rails.logger.info("You have found an object!")
end
end
ruby
irb> User.new
You have initialized an object!
=> #<User id: nil>
irb> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>
2.7 after_touch
当Active Record对象被touched就会调用after_touch回调。
ruby
class User < ApplicationRecord
after_touch do |user|
Rails.logger.info("You have touched an object")
end
end
ruby
irb> user = User.create(name: "Kuldeep")
=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
irb> user.touch
You have touched an object
=> true
可以与belongs_to一起使用:
ruby
class Book < ApplicationRecord
belongs_to :library, touch: true
after_touch do
Rails.logger.info("A Book was touched")
end
end
class Library < ApplicationRecord
has_many :books
after_touch :log_when_books_or_library_touched
private
def log_when_books_or_library_touched
Rails.logger.info("Book/Library was touched")
end
end
ruby
irb> book = Book.last
=> #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
irb> book.touch # triggers book.library.touch
A Book was touched
Book/Library was touched
=> true
3.Running Callbacks
下面这些方法触发回调:
createcreate!destroydestroy!destroy_alldestroy_bysavesave!save(validate: false)save!(validate: false)toggle!touchupdate_attributeupdate_attribute!updateupdate!valid?validate
此外,以下查找方法也会触发 after_find 回调:
allfirstfindfind_byfind_by!find_by_*find_by_*!find_by_sqllastsoletake
每次初始化类的新对象时,都会触发 after_initialize 回调。