Rails 7: upsert_allのon_duplicateオプションに生SQLを渡せるようになった(翻訳)
Railsアプリケーションでレコードを一括挿入する必要が生じることがよくあります。Rails 6では一括インポートを解決するためにinsert_all
メソッドとupsert_all
メソッドが導入されました(『Rails 6 bulk insert records』)。
upsert_all
メソッドは単に「update
」と「insert
」を合わせたものです。レコードが存在する場合は新しい属性でupdate
し、存在しない場合は新規レコードをinsert
します。
変更前
Rails 6ではupsert_all
メソッドにreturning:
オプションやunique_by:
オプションを渡せます。
returning
オプションは、insert
やupdate
が成功したすべてのレコードから返されるべきモデル属性の配列を指定します。
たとえば、id
、name
、price
という属性を持つCommodityというモデルがあるとします。Commodityモデルで3行をインポートし、更新されたname
属性とprice
属性を返すよう指定するreturning
オプションを渡すとします。これを実行するコードは以下のようになります。
result = Commodity.upsert_all(
[
{ id: 1, name: "原油", price: 51.27 },
{ id: 2, name: "銅", price: 2.84 },
{ id: 4, name: "金", price: 1480.35 }
],
)
# Bulk Update (2.3ms) INSERT INTO "commodities" ("id", "name","price")
# VALUES(1, "原油", 51.27...)
# RETURNING "name", "price"
puts result.inspect
#<ActiveRecord::Result:0x00007fb6612a1ad8 @columns=["id", "name", "price"], @rows=[[1, "原油", 51.27]....],
@hash_rows=nil, @column_types=...>
puts result.rows
[[1, "原油", 51.27], [2, "銅", 2.84], [4, "金", 1480.35]]
Rails 6では、upsert_all
に重複レコードを渡すと、デフォルトでは新しい属性でレコードをupdate
します。しかし状況によっては、重複レコードが渡されたらエラーを発生させたい場合や、カスタムSQLを実行したい場合もあります。
試しにupsert_all
メソッドにon_duplicate:
オプションを渡すと以下のエラーが発生します。
result = Commodity.upsert_all(
[
{ id: 1, name: "原油", price: 51.27 },
{ id: 2, name: "銅", price: 2.84 },
{ id: 4, name: "金", price: 1480.35 }
],
on_duplicate: :raise
)
ArgumentError (unknown keyword: :on_duplicate)
変更後
上の問題を解決するため、Rails 7でupsert_all
にon_duplicate:
オプションが追加されました(#41933)。この変更で、on_duplicate:
オプションやreturning:
オプションに生SQLクエリも渡せるようになりました。
先ほどのCommodityモデルの例を続けます。重複レコードをupsert
しようとするときに、そのコモディティの中で最も高い価格に設定したいとします。以下のようにon_duplicate:
オプションにカスタムSQLクエリを渡すことで、コモディティで最も高い価格を設定できるようになります。
Commodity.upsert_all(
[
{ id: 2, name: "銅", price: 4.84 },
{ id: 4, name: "金", price: 1380.87 },
{ id: 6, name: "アルミニウム", price: 0.35 }
],
on_duplicate: Arel.sql("price = GREATEST(commodities.price, EXCLUDED.price)")
)
Commodity.find_by_name("銅").price
#=> 4.84
Commodity.find_by_name("金").price
#=> 1480.35
上のように、銅の価格は元の2.84
より高値の4.84
に更新されますが、金の価格は変わっていません。
同様に、returning:
オプションにもカスタムクエリを渡せます。
Commodity.upsert_all(
[
{ id: 2, name: "銅", price: 4.84, created_at: Time.now, updated_at: Time.now },
{ id: 4, name: "金", price: 1380.87, created_at: Time.now, updated_at: Time.now },
{ id: 8, name: "鉄", price: 1.35, created_at: Time.now, updated_at: Time.now }
],
returning: Arel.sql("id, (xmax = '0') as inserted, name as new_name")
)
#=> <ActiveRecord::Result:0x00007f9a3061ee38 @columns=["id", "inserted", "new_name"], @rows=[[2, false, "銅"], [4, false, "金"], [8, true, "鉄"]], @hash_rows=nil, @column_types={}>
ご覧のとおり、xmax = '0'
はレコードが挿入される場合true
になり、既存のレコードではfalse
になります。
原注
上述の変更を適用できないデータベースもあります。 returning:
オプションはPostgreSQLではサポートされていますが1、SQLite3ではサポートされていません。
関連記事
-
訳注:
upsert_all
のreturning:
オプションはapi.rubyonrails.orgとedgeapi.rubyonrails.orgともに「PostgreSQLのみ」と記載されています。 ↩
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。