あるテーブルに対して複数のレコードを同時に登録したい場合、BULK INSERTを使ってデータを挿入することが多いと思います。
Railsの場合、activerecord-importというGEMがよく使われます。
問題点
データを挿入したあと、挿入したデータを取得して処理したい場合、データの値によっては絞りきれない場合があります。
その場合、あとの処理がうまく実行できない場合が・・・😿
下記のように、UNIQ制約が張ってそうなテーブルに対しては、このようなSQLで良いでしょう。
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制約が貼ってないような、曖昧なデータの場合どうしますか?
# 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を読んでみると・・・
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が入るよ、でも、サポートしてなかったら空配列を返すね」
というコメントが😇
ここで、処理を追ってみます。
-
import
メソッドが呼ばれる -
import_helper
メソッドが呼ばれる -
import_without_validations_or_callbacks
メソッドが呼ばれる - 上記のメソッド内で、各アダプター共通の
insert_many
メソッドが呼ばれる - SQLが実行され、importメソッドの結果が返る
では、MySQLアダプター内のinsert_many
メソッドをみてみます。
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でフィルターし、データを取得するだけです・・・。
# 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したデータをのちに使いたい場合は、データ取得方法を考えなければなりません!
気をつけましょう。