はじめに
Rails 6 に追加された新機能を試す第85段。 今回は、 insert_all upsert_all
編です。
Rails 6 では、データの一括登録、一括更新用メソッド insert_all
, insert_all!
, upsert_all
の3つのメソッドが追加されました。
Ruby 2.6.4, Rails 6.0.0 で確認しました。
また、データベースに PostgreSQL 10.7 を使っています。
$ rails --version
Rails 6.0.0
今回は、 title
, isbn
の2つの属性を持つBookモデルを使ってデータを一括登録、一括更新を行うことで機能を試してみます。
プロジェクトを作る
rails new rails_sandbox
cd rails_sandbox
Book モデルを作る
title
, isbn
の2つの属性を持つBook モデルを作ります。
bin/rails g model Book title isbn
isbn
にユニークインデックスをつける
isbn
にユニークインデックスをつけます。
データベースに登録されているデータと同じユニークキーを持つデータを一括更新、一括登録の中で処理した場合の動作を確認するためです。
class CreateBooks < ActiveRecord::Migration[6.0]
def change
...
add_index :books, :isbn, unique: true
end
end
seed データを作る
あらかじめデータを登録しておくため、seed データを作ります。
Book.create(
[
{ title: 'Agile Web Development with Rails', isbn: '978-1680502510' },
{ title: 'Programming Ruby', isbn: '978-1937785499' }
]
)
マイグレーションを実行し、seedデータを登録する
$ bin/rails db:create db:migrate db:seed
insert_all
insert_all
を試すためにスクリプトを作成します。
登録する2件のデータのうち2件目の isbn は、 既にデータベースに存在しています。
unique_by
で isbn
を指定しておきます。
また、メソッドの実行結果として、登録されたデータの id
と title
を返すようにします。
(returning
は、MySQL では使えません。)
now = Time.zone.now
inserted_books = Book.insert_all(
[
{ title: 'Pragmatic Programmer', isbn: '978-0201616224', created_at: now, updated_at: now },
{ title: 'Agile Web Development with Rails 5.1', isbn: '978-1680502510', created_at: now, updated_at: now }
],
unique_by: :isbn,
returning: %i[id title]
)
pp inserted_books
実際に rails console
から試してみます。rails console
終了時にデータベースのロールバックを実行して登録、更新をなかったことにするために、 -s
オプションを使います。
$ bin/rails c -s
...
irb(main):001:0> load 'scripts/insert_all.rb'
(0.3ms) BEGIN
Book Bulk Insert (0.4ms) INSERT INTO "books"("title","isbn","created_at","updated_at") VALUES ('Pragmatic Programmer', '978-0201616224', '2019-09-14 22:37:12.006051', '2019-09-14 22:37:12.006051'), ('Agile Web Development with Rails 5.1', '978-1680502510', '2019-09-14 22:37:12.006051', '2019-09-14 22:37:12.006051') ON CONFLICT ("isbn") DO NOTHING RETURNING "id","title"
#<ActiveRecord::Result:0x000055de4b559ab8
@column_types=
{"id"=>
#<ActiveModel::Type::Integer:0x000055de4a36ce90
@limit=8,
@precision=nil,
@range=-9223372036854775808...9223372036854775808,
@scale=nil>,
"title"=>
#<ActiveModel::Type::String:0x000055de4b54ca98
@limit=nil,
@precision=nil,
@scale=nil>},
@columns=["id", "title"],
@hash_rows=nil,
@rows=[[3, "Pragmatic Programmer"]]>
=> true
SQL文の最後が ON CONFLICT ("isbn") DO NOTHING RETURNING "id","title"
となっていることに注意してください。
実行結果から、 Pragmatic Programmer だけが登録されて Agile Web Development with Rails 5.1 は登録されなかったことがわかります。
実際に確認してみます。
irb(main):006:0> Book.all.map{|book| [book.id, book.title, book.isbn]}
Book Load (0.6ms) SELECT "books".* FROM "books"
=> [[1, "Agile Web Development with Rails", "978-1680502510"], [2, "Programming Ruby", "978-1937785499"], [3, "Pragmatic Programmer", "978-0201616224"]]
insert_all!
今度は、 insert_all!
を試します。
insert_all!
では、 unique_by
オプションが使えないことに注意してください。
now = Time.zone.now
inserted_books = Book.insert_all!(
[
{ title: 'Pragmatic Programmer', isbn: '978-0201616224', created_at: now, updated_at: now },
{ title: 'Agile Web Development with Rails 5.1', isbn: '978-1680502510', created_at: now, updated_at: now }
],
returning: %i[id title]
)
pp inserted_books
では、 rails console
で試してみます。 一旦、 exit で抜けて、再度 rails console
を実行して試します。
bin/rails c
irb(main):001:0> load 'scripts/insert_all_exclamation.rb'
Book Bulk Insert (0.8ms) INSERT INTO "books"("title","isbn","created_at","updated_at") VALUES ('Pragmatic Programmer', '978-0201616224', '2019-09-14 22:53:19.823710', '2019-09-14 22:53:19.823710'), ('Agile Web Development with Rails 5.1', '978-1680502510', '2019-09-14 22:53:19.823710', '2019-09-14 22:53:19.823710') RETURNING "id","title"
Traceback (most recent call last):
1: from (irb):1
ActiveRecord::RecordNotUnique (PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_books_on_isbn")
DETAIL: Key (isbn)=(978-1680502510) already exists.
今度は、 ActiveRecord::RecordNotUnique
例外が発生しました。
データが登録されなかったことも確認します。
irb(main):002:0> Book.all.map{|book| [book.id, book.title, book.isbn]}
Book Load (0.4ms) SELECT "books".* FROM "books"
=> [[1, "Agile Web Development with Rails", "978-1680502510"], [2, "Programming Ruby", "978-1937785499"]]
upsert_all
今度は、 upsert_all
を試します。
now = Time.zone.now
inserted_books = Book.upsert_all(
[
{ title: 'Pragmatic Programmer', isbn: '978-0201616224', created_at: now, updated_at: now },
{ title: 'Agile Web Development with Rails 5.1', isbn: '978-1680502510', created_at: now, updated_at: now }
],
unique_by: :isbn,
returning: %i[id title]
)
pp inserted_books
では、実行してみましょう。
$ bin/rails c -s
...
irb(main):001:0> load 'scripts/upsert_all.rb'
(0.2ms) BEGIN
Book Bulk Upsert (0.5ms) INSERT INTO "books"("title","isbn","created_at","updated_at") VALUES ('Pragmatic Programmer', '978-0201616224', '2019-09-14 23:02:19.763608', '2019-09-14 23:02:19.763608'), ('Agile Web Development with Rails 5.1', '978-1680502510', '2019-09-14 23:02:19.763608', '2019-09-14 23:02:19.763608') ON CONFLICT ("isbn") DO UPDATE SET "title"=excluded."title","created_at"=excluded."created_at","updated_at"=excluded."updated_at" RETURNING "id","title"
#<ActiveRecord::Result:0x000055de490db808
@column_types=
{"id"=>
#<ActiveModel::Type::Integer:0x000055de4b2e9328
@limit=8,
@precision=nil,
@range=-9223372036854775808...9223372036854775808,
@scale=nil>,
"title"=>
#<ActiveModel::Type::String:0x000055de490621b0
@limit=nil,
@precision=nil,
@scale=nil>},
@columns=["id", "title"],
@hash_rows=nil,
@rows=
[[9, "Pragmatic Programmer"], [1, "Agile Web Development with Rails 5.1"]]>
=> true
SQL文の最後が、 ON CONFLICT ("isbn") DO UPDATE SET ...
となっていることに注意してください。
結果から、 insert_all
と違い2件とも処理されていることがわかります。
実際に確認します。
irb(main):003:0> Book.all.map{|book| [book.id, book.title, book.isbn]}
Book Load (0.4ms) SELECT "books".* FROM "books"
=> [[2, "Programming Ruby", "978-1937785499"], [9, "Pragmatic Programmer", "978-0201616224"], [1, "Agile Web Development with Rails 5.1", "978-1680502510"]]
Pragmatic Programmer が増え、 Agile Web Development with Rails 5.1 に変更されていることがわかります。
結果を整理すると
-
insert_all
は同じユニークキーのデータは無視(登録しない)して、それ以外のデータを登録する。 -
insert_all!
は同じユニークキーのデータがあるとActiveRecord::RecordNotUnique
例外を raise し、ロールバックする。 -
upsert_all
は同じユニークキーのデータがあると更新して、データが無ければ登録する。
となります。
なお、これらのメソッドは、 Model を介さず、直接、SQLを組み立てて実行するため、 validation によるチェックや Model のコールバックは実行されません。
試したソース
試したソースは以下にあります。
https://github.com/suketa/rails_sandbox/tree/try085_insert_all