28
5

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.

BULK INSERTの戻り値にPRIMARY KEYは返らない(activerecord-import)

Posted at

あるテーブルに対して複数のレコードを同時に登録したい場合、BULK INSERTを使ってデータを挿入することが多いと思います。
Railsの場合、activerecord-importというGEMがよく使われます。

問題点

データを挿入したあと、挿入したデータを取得して処理したい場合、データの値によっては絞りきれない場合があります。
その場合、あとの処理がうまく実行できない場合が・・・😿

下記のように、UNIQ制約が張ってそうなテーブルに対しては、このようなSQLで良いでしょう。

UNIQ制約が貼ってあるデータを挿入→取得する方法
users = profile_ids.map do |profile_id|
  User.new(profile_id: profile_id)
end

# BULK INSERTでデータを挿入
User.import users

# 挿入したデータを取得する
User.where(profile_id: profile_ids)

以下のようにUNIQ制約が貼ってないような、曖昧なデータの場合どうしますか?

UNIQ制約が貼ってないデータを挿入→データを取得する方法
# UserのnameはUNIQ制約がない
users = names.uniq.map do |name|
  User.new(name: name)
end

# BULK INSERTでデータを挿入
User.import users

# 挿入したデータを取得する
User.where(name: names)

同じnameのレコードがすでに挿入されていた場合、余計なレコードまで取得してしまいます

そもそもの問題

activerecord-importとMySQLを組み合わせると、挿入したデータのidsが格納されないためです。

# idsが空っぽ!
[1] pry > User.import users
=> #<struct ActiveRecord::Import::Result failed_instances=[], num_inserts=5, ids=[]>

なんで、idsに挿入したデータの情報が入らないのか?と思い、Gemを読んでみると・・・

lib/activerecord-import/import.rb
class ActiveRecord::Base
  class << self
    # = Returns
    # This returns an object which responds to +failed_instances+ and +num_inserts+.
    # * failed_instances - an array of objects that fails validation and were not committed to the database. An empty array if no validation is performed.
    # * num_inserts - the number of insert statements it took to import the data
    # * ids - the primary keys of the imported ids if the adapter supports it, otherwise an empty array.
    def import(*args)
      if args.first.is_a?( Array ) && args.first.first.is_a?(ActiveRecord::Base)
        options = {}
        options.merge!( args.pop ) if args.last.is_a?(Hash)

        models = args.first
        import_helper(models, options)
      else
        import_helper(*args)
      end
    end
  end
end

「もし、アダプターがサポートしていたら、primary keyが入るよ、でも、サポートしてなかったら空配列を返すね

というコメントが😇

ここで、処理を追ってみます。

  1. importメソッドが呼ばれる
  2. import_helperメソッドが呼ばれる
  3. import_without_validations_or_callbacksメソッドが呼ばれる
  4. 上記のメソッド内で、各アダプター共通のinsert_manyメソッドが呼ばれる
  5. SQLが実行され、importメソッドの結果が返る

では、MySQLアダプター内のinsert_manyメソッドをみてみます。

lib/activerecord-import/adapters/mysql_adapter.rb
module ActiveRecord::Import::MysqlAdapter
  def insert_many( sql, values, options = {}, *args ) # :nodoc:
    # the number of inserts default
    number_of_inserts = 0

    base_sql, post_sql = if sql.is_a?( String )
      [sql, '']
    elsif sql.is_a?( Array )
      [sql.shift, sql.join( ' ' )]
    end

    # 〜(略)〜

    [number_of_inserts, []]
  end
end

空配列が固定で返ってますね・・・😇 というわけで、MySQLは対応していません。
現在、ids対応してるDBはpostgresqlだけです。

解決策

UNIQ制約が貼ってないような、曖昧なデータを取得する方法です。
挿入前後のIDを取得し、最後のデータ取得ではidでフィルターし、データを取得するだけです・・・。

UNIQ制約が貼ってあるデータを挿入→取得する方法
# UserのnameはUNIQ制約がない
users = names.uniq.map do |name|
  User.new(name: name)
end

# BULK INSERTが挿入する前のIDを取得し、+1する
before_id = User.last.try(:id).to_i + 1

# BULK INSERTでデータを挿入
User.import users

# BULK INSERTが挿入した後のIDを取得する
after_id = User.last.id

# 挿入前後のidで絞り、挿入したデータを取得する
User.where(id: before_id..after_id, name: names)

まとめ

DBにMySQLを使い、activerecord-importを使ったBULK INSERTは、戻り値にPRIMARY KEYは返らないです(タイトルに書いてあるまま・・・)

BULK INSERTしたデータをのちに使いたい場合は、データ取得方法を考えなければなりません!
気をつけましょう。

28
5
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
28
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?