Help us learn about your current experience with the documentation. Take the survey.

批量更新数据库对象

你可以使用一个或多个列的新值来更新多个数据库对象。 一种方法是使用 Relation#update_all

user.issues.open.update_all(due_date: 7.days.from_now) # (1)
user.issues.update_all('relative_position = relative_position + 1') # (2)

如果你无法将更新表示为静态值 (1) 或计算 (2), 使用 UPDATE FROM 来表达在单个查询中用不同值更新多行的需求。 创建一个临时表或公共表表达式 (CTE),并将其用作更新的数据源:

with updates(obj_id, new_title, new_weight) as (
  values (1 :: integer, 'Very difficult issue' :: text, 8 :: integer),
         (2, 'Very easy issue', 1)
)
update issues
  set title = new_title, weight = new_weight
  from updates
  where id = obj_id

你无法在 ActiveRecord 中表达这一点,或者通过降级到 Arel, 因为 UpdateManager 不支持 update from。但是,我们提供了一个抽象来帮助你生成这类更新:Gitlab::Database::BulkUpdate。 这个抽象会构建类似前面的示例查询,并使用绑定参数来避免 SQL 注入。

使用方法

要使用 Gitlab::Database::BulkUpdate,我们需要:

  • 要更新的列列表。
  • 从对象(或 ID)到该对象要设置的新值的映射。
  • 确定每个对象所属表的方法。

例如,我们可以通过调用 object.class.table_name 来确定表的方式表达示例查询:

issue_a = Issue.find(..)
issue_b = Issue.find(..)

# 发送单个查询:
::Gitlab::Database::BulkUpdate.execute(%i[title weight], {
  issue_a => { title: 'Very difficult issue', weight: 8 },
  issue_b => { title: 'Very easy issue', weight: 1 }
})

如果你传递的是不同类型的对象集合,只要这些更新对它们都有意义:

issue_a = Issue.find(..)
issue_b = Issue.find(..)
merge_request = MergeRequest.find(..)

# 发送两个查询
::Gitlab::Database::BulkUpdate.execute(%i[title], {
  issue_a => { title: 'A' },
  issue_b => { title: 'B' },
  merge_request => { title: 'B' }
})

如果你的对象没有返回正确的模型类,例如它们是联合查询的一部分, 那么在代码块中明确指定模型类:

bazzes = params
objects = Foo.from_union([
    Foo.select("id, 'foo' as object_type").where(quux: true),
    Bar.select("id, 'bar' as object_type").where(wibble: true)
    ])
# 此时,所有对象都是 Foo 的实例,即使来自 Bar 表的对象也是如此
mapping = objects.to_h { |obj| [obj, bazzes[obj.id]] }

# 最多发送 2 个查询
::Gitlab::Database::BulkUpdate.execute(%i[baz], mapping) do |obj|
  obj.object_type.constantize
end

注意事项

这个工具非常底层,直接操作原始列值。如果你要实现它,应该考虑这些问题:

  • 枚举和状态字段必须转换为它们的底层表示形式。
  • 不支持嵌套关联。
  • 不会调用任何验证或钩子。