複数のデータベースオブジェクトの更新

GitLab 13.5 で導入されました

複数のデータベースオブジェクトを、ひとつあるいは複数のカラムの新しい値で更新することができます。一つの方法は、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 を使用して、1 つのクエリで複数の行を異なる値で更新する必要性を表現します。一時テーブルまたは共通テーブル式(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ではUpdateManagerupdate fromをサポートしていないため、これを表現することはできません。しかし、ActiveRecordでは、このような更新を生成するのに役立つ抽象化機能を提供しています:Gitlab::Database::BulkUpdate。この抽象化は、前の例のようにクエリを構築し、SQLインジェクションを避けるためにバインディングパラメータを使用します。

使用方法

Gitlab::Database::BulkUpdateを使用するには、次のものが必要です:

  • 更新する列のリスト。
  • オブジェクト (または ID) から、そのオブジェクトに設定する新しい値へのマッピング。
  • 各オブジェクトのテーブルを決定する方法。

例えば、object.class.table_name を呼び出すことで、テーブルを決定する方法でクエリ例を表現することができます:

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

# Issues a single query:
::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(..)

# Issues two queries
::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)
    ])
# At this point, all the objects are instances of Foo, even the ones from the
# Bar table
mapping = objects.to_h { |obj| [obj, bazzes[obj.id]] }

# Issues at most 2 queries
::Gitlab::Database::BulkUpdate.execute(%i[baz], mapping) do |obj|
  obj.object_type.constantize
end

注意点

このツールは非常に低レベルで、生のカラム値を直接オペレーションします。実装する場合は、これらのイシューを考慮する必要があります:

  • 列挙と状態フィールドは、その基礎となる表現に変換されなければなりません。
  • 入れ子の関連はサポートされていません。
  • バリデーションやフックは呼び出されません。