15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rails6 のちょい足しな新機能を試す85(insert_all upsert_all編)

Posted at

はじめに

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 にユニークインデックスをつけます。

データベースに登録されているデータと同じユニークキーを持つデータを一括更新、一括登録の中で処理した場合の動作を確認するためです。

db/migrate/20190908115949_create_books.rb
class CreateBooks < ActiveRecord::Migration[6.0]
  def change
    ...
    add_index :books, :isbn, unique: true
  end
end

seed データを作る

あらかじめデータを登録しておくため、seed データを作ります。

db/seeds.rb
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_byisbn を指定しておきます。
また、メソッドの実行結果として、登録されたデータの idtitle を返すようにします。

(returning は、MySQL では使えません。)

scripts/insert_all.rb
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 オプションが使えないことに注意してください。

scripts/insert_all_exclamation.rb
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 を試します。

scripts/upsert_all.rb
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

参考情報

15
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?