重读 Rails Guide 2

Active Record 回调

回调是在对象生命周期的某些时刻被调用的方法。通过回调,我们可以编写在创建、保存、更新、删除、验证或从数据库中加载 Active Record 对象时执行的代码。

class User < ApplicationRecord
  before_create do
    self.name = login.capitalize if name.blank?
  end

  after_validation :set_location, on: [ :create, :update ]

  private

    def set_location
      self.location = LocationService.query(self)
    end
end

如果模型中的关联有 dependent: :destroy 配置项,那么 before_destroy 需要声明在其之前,或者在 before_destroy 宏声明上添加 prepend: true 选项。

class User < ActiveRecord::Base
  has_many :authorizations, dependent: :delete_all
  before_destroy :remove_external_account

  def remove_external_account
    authorization_uuid = authorizations.last.uuid
    return unless authorization_uuid
    Rails.log.info( [DebugCode] before call Sidekiq worker”’)
    RemoveExternalAccountWorker.perform_async(user.id, authorization_uuid)
  end
end
ser Load (0.4ms) SELECT “users”.* FROM “users” WHERE “users”.”id” = 1 LIMIT 1 [[“id”, 1]]
SQL (0.6ms) DELETE FROM “authorizations” WHERE “authorizations”.”user_id” = 1 [[“user_id”, 1]]
SQL (0.4ms) DELETE FROM “users” WHERE “users”.”id” = 1 [[“id”, 1]]
[DebugCode] before “call Sidekiq worker”
Authorization Load (0.4ms) SELECT “authorizations”.”uuid” FROM “authorizations” WHERE “authorizations”.”user_id” = 1 AND “authorizations”.”provider” = 0 ORDER BY “authorizations”.”id” ASC LIMIT 1 [[“user_id”, 1], [“provider”, 0]]

这里 authorizations 先被删除了,然后才执行到 before_destroy :remove_external_account 这里,而 remove_external_account 想读取 authorizations 的数据,就会出错。正确写法如下:

class User < ActiveRecord::Base
  has_many :authorizations, dependent: :delete_all
  before_destroy :remove_external_account, prepend: true
end

after_initialize 和 after_find 回调

  • 不管是通过直接使用 new 方法还是从数据库加载记录,都会调用 after_initialize 回调
  • 从数据库中加载记录时,会调用 after_find 回调
  • 会先调用 after_find,再调用 after_initialize
  • 不存在 before_findbefore_initialize

after_touch

在对象执行 touch 方法之后会触发该回调,可以配合 belongs_totouch: true

跳过回调

  • decrement
  • decrement_counter
  • delete
  • delete_all
  • increment
  • increment_counter
  • toggle
  • update_column
  • update_columns
  • update_all
  • update_counters

Rails 手册上给的建议是慎用这些方法,因为一些重要的业务逻辑可能保存在回调中。

模型中的验证,回调和数据库操作都被包裹在事务中,排队执行,期间任何回调引发异常都会造成整个回调链的回滚,当然也可以手动使用 throw :abort 来停止回调链。

需要注意的地方是当回调链停止后,Rails 会重新抛出除 ActiveRecord::RollbackActiveRecord::RecordInvalid 之外的其他异常。这可能导致那些预期返回 true 或 false 的方法,比如 saveupdate_attributes 代码出错。

条件回调

class Order < ApplicationRecord
  before_save :normalize_card_number, if: Proc.new { paid_with_card? }
end

而根据 DRY 原则,如果我们创建的回调可以被其它模型使用,最好是单独写回调类。

class PictureFileCallbacks
  def self.after_destroy(picture_file)
    if File.exist?(picture_file.filepath)
      File.delete(picture_file.filepath)
    end
  end
end

class PictureFile < ApplicationRecord
  after_destroy PictureFileCallbacks
end

事务回调

after_commitafter_rollback 这两个回调会在数据库事务完成时触发。和 after_save 回调区别在于它们在数据库变更已经提交或回滚后才会执行,常用于 Active Record 模型需要和数据库事务之外的系统交互的场景。

如果 after_destroy 回调执行后应用引发异常,事务就会回滚,文件会被删除,模型会保持不一致的状态。通过使用 after_commit 回调,我们可以解决这个问题:

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

由于只在执行创建、更新或删除动作时触发 after_commit 回调是很常见的,这些操作都拥有别名:

  • after_create_commit
  • after_update_commit
  • after_destroy_commit

注意

  1. 在事务中创建、更新或删除模型时会调用 after_commitafter_rollback 回调。然而,如果其中有一个回调引发异常,异常会向上冒泡,后续 after_commitafter_rollback 回调不再执行。因此,如果回调代码可能引发异常,就需要在回调中救援并进行适当处理,以便让其他回调继续运行。
  2. after_commitafter_rollback 内部执行的代码,本身不包含在事务中。
  3. 在同一个模型中定义 after_create_commitafter_update_commit,只有最后定义的会生效
# Wrong
class User < ApplicationRecord
  after_create_commit :log_user_saved_to_db
  after_update_commit :log_user_saved_to_db

  private
  def log_user_saved_to_db
    puts 'User was saved to database'
  end
end

# Right
class User < ApplicationRecord
  after_commit :log_user_saved_to_db, on: [:create, :update]
end

Active Record 关联

模型之间的关联能让常规操作变得更简单。关联使用宏式调用实现,用声明的形式为模型添加功能。Rails 会维护两个模型之间的 主键-外键 关系,而且还会向模型中添加很多实用的方法。

关联的类型

  • belongs_to
  • has_one
  • has_many
  • has_many :through
  • has_one :through
  • has_and_belongs_to_many

小技巧和注意事项

控制缓存

关联添加的方法都会使用缓存,记录最近一次查询的结果,以备后用。缓存还会在方法之间共享。

author.books                 # 从数据库中检索图书
author.books.size            # 使用缓存的图书副本
author.books.reload.empty?   # 丢掉缓存的图书副本
                             # 重新从数据库中检索

避免命名冲突

所以关联的名字不能和 ActiveRecord::Base 中的实例方法同名。例如,关联的名字不能为 attributesconnection

控制关联的作用域

默认情况下,关联只会查找当前模块作用域中的对象。如果在模块中定义 Active Record 模型,知道这一点很重要。

下面这段代码就不能正常运行,因为 Supplier 和 Account 在不同的作用域中:

module MyApplication
  module Business
    class Supplier < ApplicationRecord
       has_one :account
    end
  end

  module Billing
    class Account < ApplicationRecord
       belongs_to :supplier
    end
  end
end

Active Record 查询接口

相关链接

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @Victor Sep 29, 2018

奉献爱心