窓際BLOG

プログラミングの学習メモや書籍の感想等を公開していきます。

RubyonRails:activerecord-importを使って複数レコードを一括登録する(BULK INSERT)

みなさん、こんにちは。まどぎわです(・∀・)
バッチ処理等で複数のレコードを一括で登録する際にどのようなコードを書いていますか?今回は、そんなときに便利なactiverecord-importの使い方を調べたのでメモしておきますφ(..)

activerecord-importの使い方

改善対象のコード例

複数のレコードを取得して登録する処理でパッと思いつくのは下記のようなコードでしょうか?

 # 今月以降に登録された本を新作本を管理するTBLに登録する
class NewBook < ApplicationRecord
  def self.make_new_books
    books = Book.where('created_at >= ?', Time.current.beginning_of_month)
    books.each do |book|
      NewBook.create(book_id: book.id)
    end
  end
end

しかし、これを実行すると下記のようにレコード数分のINSERTを行うSQLが発行され、効率がよくありません。。。

  (0.1ms)  begin transaction
  SQL (1.5ms)  INSERT INTO "new_books" ("book_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["book_id", 2], ["created_at", "2018-04-21 08:41:30.705444"], ["updated_at", "2018-04-21 08:41:30.705444"]]
   (1.0ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.7ms)  INSERT INTO "new_books" ("book_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["book_id", 3], ["created_at", "2018-04-21 08:41:30.710686"], ["updated_at", "2018-04-21 08:41:30.710686"]]
   (1.1ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.5ms)  INSERT INTO "new_books" ("book_id", "created_at", "updated_at") VALUES (?, ?, ?)  [["book_id", 6], ["created_at", "2018-04-21 08:41:30.715154"], ["updated_at", "2018-04-21 08:41:30.715154"]]
   (0.9ms)  commit transaction
以下省略

activerecord-importを使う準備

こんな時に便利なのが、activerecord-importというGemです!

github.com

使い方は、簡単でGemfileに下記を追記してbundle installを実行するだけです。

gem 'activerecord-import' 

activerecord-importを使ってコードを修正してみる

上で書いたコードをactiverecord-importで書き直すとこんな感じ。

class NewBook < ActiveRecord::Base
  # 今月以降に登録された本を新作本を管理するTBLに登録する
  def self.make_new_books
    destroy_all
    start_at = Time.current.iso8601(2)
    book_ids = Book.where('created_at >= ?', Time.current.beginning_of_month).pluck(:id)
    NewBook.import book_ids.map{ |book_id| NewBook.new(book_id: book_id) }
    end_at = Time.current.iso8601(2)
  end
end

実行されるSQLは下記のような感じです。INSERT文が一つになっていて効率的ですね(・∀・)

  Class Create Many Without Validations Or Callbacks (2.5ms)  INSERT INTO "new_books" ("id","book_id","created_at","updated_at") VALUES (NULL,2,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,3,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,6,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,7,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,8,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,9,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529'),(NULL,10,'2018-04-21 14:04:17.913385','2018-04-21 14:04:17.913529')

修正前後のベンチマーク

レコード件数は、7件とかなりイマイチですが、、、ベンチマークをとってみました。

case start_at end_at time
nomal 2018-04-21T14:09:51.75 2018-04-21T14:09:51.82 0.7s
user import 2018-04-21T14:10:29.37 2018-04-21T14:10:29.41 0.4s

これぐらいの件数でも意外と差がでますね!(・∀・)
やはりactiverecord-importの方が効率が良いみたいです!

おまけundefined methodimport' for Model`と出る場合

良くわからないけど、rubyのバージョンを2.4.0から2.4.3に変更したら治った・・・。

おわりに

夜間バッチでランキングを作ったりと実際の業務だと一括登録の処理は実装する機会が多そうですね!
一括登録処理は結構重くなりがちになりそうなので、activerecord-importを使って効率的な処理で行えるようにしたいですね(・∀・)