Ruby ActiveRecord 如何在子记录回调更新父记录时避免数据库延迟/锁定

Ruby ActiveRecord how to avoid Database delay/lock when child record callback is updating parent record

提问人:Shankar 提问时间:11/11/2023 最后编辑:Shankar 更新时间:11/15/2023 访问量:42

问:

我有一个 Sidekiq 项目,正在运行一个任务,创建多个子记录并更新此子对象所属的父记录。数据库是 Postgres。

架构如下。创建 Child 记录时,有一个 before_create 方法可以更新 Parent 中的标志。父级具有更新时间戳的before_save方法。

# t.boolean "is_updated", default: false
#  id                          :integer          not null, primary key
#t.datetime "updated_at"
class Parent < ActiveRecord::Base
  has_many :child

  def update_flag
   self.is_updated = true
  end

  before_save : set_updated_at

  def set_updated_at
    self.updated_at = Time.current 
  end
end


class ChildRecord < ActiveRecord::Base
  belongs_to :parent
  before_create :update_parent_flag

  def update_parent_flag
    if self.parent.try(:update_flag)
      self.parent.save!
    end
  end
end

当 Sidekiq 作业仅创建一个子记录时,不会出现错误。但是,当作业尝试创建更大的子记录(在一个示例中为 35 个)时,作业将长时间处于繁忙状态。我们可以看到 Postgres 连接正在等待 Parent 更新中的锁定。

更新Sidekiq 作业在单个事务中更新或创建一批子记录。

def perform(params, options = {})
  children = params[:children] 
  #New transaction
  ActiveRecord::Base.transaction do
    children.each do |child|
      return_code, error = create_or_update_child_record(child)
      if return_code != :created
        Rails.logger.info error
      return
     end
  end
 end

以下是数据库中的阻塞语句。

UPDATE "Parent" SET "updated_at" = $1 WHERE "Parent"."id" = $2

创建多个子记录时如何避免此锁定?有没有更好的设计?

Ruby-on-Rails 轨道-ActiveRecord SideKiq

评论

1赞 engineersmnky 11/11/2023
update_flag似乎尝试创建一个 在创建子项之前,它会调用,然后调用 ,它试图创建一个 ....我很惊讶你没有最终进入.“有没有更好的设计?” stackoverflow.com/questions/6736265/......Childupdate_parent_flagupdate_flagChildSystemStackError
0赞 Shankar 11/11/2023
@engineersmnky我复制了错误的例子。我现在已经纠正了它。只是更新类中的布尔字段。对于造成混乱,我深表歉意。update_flagis_updatedParent
1赞 dbugger 11/11/2023
不要从子记录更新父记录 -- 在“在末尾”批处理中执行一次
0赞 Shankar 11/11/2023
@dbugger谢谢。这是我的选择之一。但是,父更新至关重要,将其作为回调可以确保在我创建子记录的任何位置更新它。
1赞 Schwern 11/11/2023
是否可以删除在父项中具有更新标志的必要性?我们需要更多地了解它的目的。另外,都是同一个父级吗?params[:children]

答:

2赞 Schwern 11/11/2023 #1

无论如何,一个接一个地插入每个子项都是低效的。您需要一种批量插入 Child 对象的方法。这种批量插入方法在最后只会更新其父项一次。

我更喜欢 activerecord-import 而不是 Rails 的insert_all,主要是因为它可以进行模型验证。

# Make the Child models, but do not insert them.
children = [Child.new(...), Child.new(...), ...]

# Validate and insert all the children in bulk
Child.import! children, validate: true

# Get their Parent's ids
parent_ids = children.map(&:parent_id).uniq

# Update their Parents
Parent
  .where(id: parent_ids)
  .update_all(
    is_updated: true,
    # update_all does not update updated_at
    updated_at: Time. current
  )

这将为子项执行一次插入,为其父项执行一次更新。

注意:考虑是否需要 is_updated 标志。您可以仅依靠updated_at时间戳吗?