首页 > 解决方案 > Rails 的模型挂钩会等到数据库事务完成吗?

问题描述

我有一个带after_saveafter_commit钩的模型。钩子在数据库中存储了钩子所需的after_save一些信息after_commit

假设所有数据库事务在after_commit钩子触发之前完成是否安全?

这里有一个小例子来说明这个问题:

class TicketComment < ApplicationRecord
  after_save :extract_mentions
  after_commit :notify

  def extract_mentions
    current_mentions = mentions.map(&:user).map(&:username)
    new_mentions = description.scan(/(?<=^|(?<=[^a-zA-ZÀ-ž0-9_-]))@([a-zA-ZÀ-ž]+[a-zA-ZÀ-ž0-9_]+)/).flatten

    mentions_to_add = new_mentions - current_mentions
    mentions_to_remove = current_mentions - new_mentions

    users_to_add = User.select(:id).where("username IN (?)", mentions_to_add.flatten).map(&:id)
    users_to_remove = User.select(:id).where("username IN (?)", mentions_to_remove.flatten).map(&:id)

    users_to_add&.each do |id_to_add|
      mentions.create(user_id: id_to_add)
    end

    mentions.where(user_id: users_to_remove).destroy_all
  end

  def notify
    user_ids = ticket.participants.pluck(:id) - [Current.user.id]
    TicketCommentMailer.comment_added(id, user_ids).deliver_later
  end
end

请注意,此示例已简化。-partextract_mentions是跨多个模型使用的模型问题,因此我无法在-hooknotify内运行代码。extract_mentions

标签: ruby-on-rails

解决方案


为了通过一些参考扩展我的评论,是的,您可以假设所有数据库事务在您的#after_commit回调运行之前已经完成。这是active_record/transaction.rbgithub)中的评论:

#after_commit 回调在事务提交后立即在事务中保存或销毁的每条记录上调用。在事务或保存点回滚后,立即对事务中保存或销毁的每条记录调用#after_rollback 回调。

这些回调对于与其他系统交互很有用,因为您将保证回调仅在数据库处于永久状态时执行。

事务代码有点复杂,因为它处理嵌套事务、保存点等。不过,最终,在TransactionManager.commit_transactionactive_record/connect_adapters/abstract/transaction.rb)中,管理器确实在运行#after_commit回调之前执行了特定于数据库的提交操作与父事务关联的所有唯一记录 ID。

def commit_transaction
  @connection.lock.synchronize do
    ...
    transaction.commit  # executes database-specific commit action
    transaction.commit_records # executes after_commit callbacks
  end
end

所有保存和销毁操作,包括它们的持久性回调,都隐式包装在事务块中。根据 Rails 中的另一条评论active_record/transaction.rb

#transaction 调用可以嵌套。默认情况下,这会使嵌套事务块中的所有数据库语句成为父事务的一部分。

因此,您的回调中由#createand生成的数据库语句与原始父事务同时提交,并且可以在回调中依赖。一个简化的例子来说明:#destroy_all#after_save#after_commit

class Foo < ApplicationRecord
  after_save :do_stuff
  after_commit :check

  def do_stuff
    Bar.create!(name: 'bar')
  end

  def check
    Rails.logger.info "Bar count: #{Bar.count}"
    Rails.logger.info "name: #{Bar.first.name}"
  end
end

输出日志:

> Foo.create!(name: "Jerry")
  (0.0ms)  begin transaction
  Foo Create (0.3ms)  INSERT INTO "foos" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "Jerry"], ["created_at", "2020-09-16 01:49:57.480367"], ["updated_at", "2020-09-16 01:49:57.480367"]]
  Bar Create (0.2ms)  INSERT INTO "bars" ("name", "created_at", "updated_at") VALUES (?, ?, ?)  [["name", "bar"], ["created_at", "2020-09-16 01:49:57.485953"], ["updated_at", "2020-09-16 01:49:57.485953"]]
 (2.3ms)  commit transaction
 (0.1ms)  SELECT COUNT(*) FROM "bars"
Bar count: 1
  Bar Load (0.1ms)  SELECT "bars".* FROM "bars" ORDER BY "bars"."id" ASC LIMIT ?  [["LIMIT", 1]]
bar

请注意,对于#after_commit在事务中创建、更新或销毁的任何模型,当然都会调用任何回调。因此,如果Bar还定义了#after_commit回调,它们也会运行。


推荐阅读