ruby-on-rails - Rails 的模型挂钩会等到数据库事务完成吗?
问题描述
我有一个带after_save
和after_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
解决方案
为了通过一些参考扩展我的评论,是的,您可以假设所有数据库事务在您的#after_commit
回调运行之前已经完成。这是active_record/transaction.rb
(github)中的评论:
#after_commit 回调在事务提交后立即在事务中保存或销毁的每条记录上调用。在事务或保存点回滚后,立即对事务中保存或销毁的每条记录调用#after_rollback 回调。
这些回调对于与其他系统交互很有用,因为您将保证回调仅在数据库处于永久状态时执行。
事务代码有点复杂,因为它处理嵌套事务、保存点等。不过,最终,在TransactionManager.commit_transaction
(active_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 调用可以嵌套。默认情况下,这会使嵌套事务块中的所有数据库语句成为父事务的一部分。
因此,您的回调中由#create
and生成的数据库语句与原始父事务同时提交,并且可以在回调中依赖。一个简化的例子来说明:#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
回调,它们也会运行。
推荐阅读
- c# - 在 Dynamics CRM 中分解日期
- haskell - 对不同的数据类型构造函数使用不同的最小完整定义
- java - 滚动时如何在工具栏下方固定线性布局
- macos - Visual Studio 中 Mac 中 xamarin.forms 的 Nuget 包
- php - PHP - File_get_contents 未检测到 CURL 响应
- mysql - 用于聚合结果的 SQL 查询
- php - html href 中的 PHP 变量
- python - 文件生成没有限制?
- django - 为什么 DRF 自定义异常会引发 500 而不是 202?
- python - 在 Cloud Function 的文件中运行 Python 脚本