Active Recordのinsert_allをto_sqlする方法

背景

あるテーブルの200万件くらいのレコードを、別のテーブルに移行する必要があった。 移行バッチを実装するにあたって、find_in_batchesのバッチサイズやバルクインサートの単位を、中間オブジェクトや生成されるINSERT文のデータサイズから決めたかった。

find_in_batchesにおけるバッチサイズは移行バッチを実行するサーバーに割り当てるメモリ量を左右するし、PostgreSQLは実行可能なクエリサイズの上限を決めるオプション(MySQLで言うところのmax_allowed_packet)はなく、プロセスに割り当てられているメモリがそれにあたるため。

今回は特にPostgreSQLで実行可能なクエリサイズを上限の範囲内に収まるかどうかを検証する課程で、ActiveRecordのinsert_allメソッドからINSERT文をdumpする方法を模索したので、備忘として書いておきたい。

やったこと

find_in_batchesの各バッチの中で、insert_allメソッドを使っていたので、作成されるINSERT文を文字列として取得することにした。 参照系のクエリだとクエリの実行前にto_sqlできるが、insert_allなどの書き込み系のクエリだと呼び出し時に実行されてしまう。 それらしき公開APIなども存在しなかったので、INSERT文をdumpする方法を探してみた。

Active Recordのソースコードを見てみると、insert_allメソッドの中でInsertAllクラスのインスタンスが呼び出されており、このInsertAllクラスのprivateメソッドにto_sqlが実装されていることがわかった。

insert_allで渡す引数をそのまま、ActiveRecord::InsertAll.newとしてto_sqlメソッドを呼び出したところ、ほしかったINSERT文を出力することができた。

to_sqlする方法

以下、例

# 内部APIを使って、insert_allメソッドをto_sqlする 
query = ActiveRecord::InsertAll.new(対象のモデル, 保存対象の配列, on_duplicate: 重複時の挙動).send(:to_sql)

puts query

得たクエリ文字列を使って、byteサイズを出したい場合は以下のようにする

# 内部APIを使って、insert_allメソッドをto_sqlする 
query = ActiveRecord::InsertAll.new(対象のモデル, 保存対象の配列, on_duplicate: 重複時の挙動).send(:to_sql)

# 文字列のバイト数を取る
puts query.bytesize