テーブルへの一括挿入
一度に大量のレコードを保存する必要があることがありますが、コレクションを繰り返し処理したり各レコードを個別に保存したりするのは非効率的です。Rails 6では行レベルでオペレーションを行う(つまりHash
オブジェクトを使う)insert_all
が登場したため、GitLabはActiveRecord
オブジェクトを安全かつ簡単に一括挿入できるAPIセットを追加しました。
一括挿入のためのApplicationRecord
の準備
モデルクラスが一括挿入 API を利用するためには、まずBulkInsertSafe
をインクルードしなければなりません:
class MyModel < ApplicationRecord
# other includes here
# ...
include BulkInsertSafe # include this last
# ...
end
BulkInsertSafe
関数には2つの関数があります:
- モデルクラスに対してチェックを行い、一括挿入に関して安全ではないActiveRecord APIを使用していないことを確認します(詳細は後述します)。
- 新しいクラスメソッド
bulk_insert!
とbulk_upsert!
が追加され、一度に多くのレコードを挿入することができます。
レコードの挿入にはbulk_insert!
とbulk_upsert!
ターゲット・クラスがBulkInsertSafe
によるチェックをパスすれば、次のように ActiveRecord モデル・オブジェクトの配列を挿入できます:
records = [MyModel.new, ...]
MyModel.bulk_insert!(records)
bulk_insert!
、常に_新しいレコードを_挿入しようとします。既存のレコードを新しい値で置き換え、まだ存在しないレコードを挿入したい場合は、bulk_upsert!
を使用してください:
records = [MyModel.new, existing_model, ...]
MyModel.bulk_upsert!(records, unique_by: [:name])
この例では、unique_by
は、レコードが一意であるとみなされるカラムを指定し、挿入前に存在していた場合は更新されます。例えば、existing_model
name
属性があり、同じ name
値をname
持つレコードが name
既に存在する場合、そのフィールドは. existing_model
unique_by
パラメータはSymbol
として渡すこともでき、その場合はカラムが一意であるとみなされるデータベースインデックスを指定します:
MyModel.bulk_insert!(records, unique_by: :index_on_name)
レコードの検証
bulk_insert!
メソッドは、records
がトランザクションで挿入されることを保証し、挿入前に各レコードのバリデーションを実行します。バリデーションに失敗したレコードがあるとエラーが発生し、トランザクションはロールバックされます。:validate
オプションでバリデーションをオフにすることができます:
MyModel.bulk_insert!(records, validate: false)
バッチサイズ設定
records
の数が所定の閾値を超える場合、挿入は複数のバッチで行われます。デフォルトのバッチ・サイズはBulkInsertSafe::DEFAULT_BATCH_SIZE
で定義されています。デフォルトのしきい値を500とすると、950レコードを挿入すると、2つのバッチが順次書き込まれることになります(それぞれ500と450のサイズです)。:batch_size
オプションでデフォルトのバッチサイズを上書きすることができます:
MyModel.bulk_insert!(records, batch_size: 100)
同じ数の950レコードを挿入した場合、10バッチが書き込まれます。これは、INSERT
発生するステートメントの INSERT
数にも影響するので、INSERT
あなたのコードに与えるかもしれないパフォーマンスの影響を測定することを確認して INSERT
ください。データベースが処理しなければならないステートメントの数と、各INSERT
のサイズとコストのINSERT
間にはトレードオフが INSERT
あります。
重複レコードの処理
bulk_insert!
にのみ適用されます。 既存のレコードを更新する場合は、代わりにbulk_upsert!
を使用してください。挿入しようとしているレコードの中に既に存在するものがあり、主キーの競合が発生する可能性があります。この問題にアドレスするには、エラーを発生させて高速に失敗させる方法と、重複レコードをスキップする方法があります。bulk_insert!
のデフォルトの動作は、高速に失敗してActiveRecord::RecordNotUnique
エラーを発生させることです。
これが望ましくない場合は、skip_duplicates
フラグで重複レコードをスキップすることができます:
MyModel.bulk_insert!(records, skip_duplicates: true)
安全な一括挿入の要件
ActiveRecordの永続化APIの大部分は、コールバックという概念を中心に構築されています。これらのコールバックの多くは、save
やcreate
のようなモデルのライフサイクルイベントに応じて発生します。これらのコールバックは、保存または作成されるインスタンスごとに呼び出されることを想定しているため、一括挿入では使用できません。これらのイベントは、レコードが一括挿入されるときには発火しないため、現在のところ、これらの使用を禁止しています。
どのコールバックが明示的に許可されるかは、BulkInsertSafe
で定義されています。詳細はモジュールのソース・コードを参照してください。クラスが明示的に安全であると指定されていないコールバックを使用し、include BulkInsertSafe
、アプリケーションはエラーで失敗します。
BulkInsertSafe
対してInsertAll
内部的には、BulkInsertSafe
はInsertAll
をベースにしています。その決断を助けるために、これらのクラスの主な違いを以下の表に示します。
入力タイプ | 入力の検証 | バッチサイズの指定 | コールバックをバイパス可能 | トランザクション | |
---|---|---|---|---|---|
bulk_insert! | ActiveRecordオブジェクト | はい(オプション) | はい(オプション) | いいえ(安全でないコールバックの使用を防ぎます) | はい |
insert_all! | 属性ハッシュ | なし | なし | はい | はい |
まとめると、BulkInsertSafe
、一括挿入はActiveRecordオブジェクトと挿入の通常の動作に近づきます。しかし、生のデータを一括挿入するだけであれば、insert_all
の方が効率的です。
has_many
関連付けの一括挿入
一般的なユースケースは、リレーションのオーナー側を通して関連付けられたリレーションのコレクションを保存することです。オーナー側のリレーションは、has_many
クラスメソッドを通してオーナーに関連付けられます:
owner = OwnerModel.new(owned_relations: array_of_owned_relations)
# saves all `owned_relations` one-by-one
owner.save!
この場合、owned_relations
の各レコードに対して、INSERT
とトランザクションを1つずつイシューすることになり、array_of_owned_relations
が大きい場合には非効率的です。これを改善するために、BulkInsertableAssociations
関数を使用して、オーナーが一括挿入に安全な関連を定義することを宣言できます:
class OwnerModel < ApplicationRecord
# other includes here
# ...
include BulkInsertableAssociations # include this last
has_many :my_models
end
ここで、my_models
は、一括挿入を行うために(前述のように)BulkInsertSafe
を宣言しなければなりません。これで、以下のように、まだ保存されていないレコードを挿入することができます:
BulkInsertableAssociations.with_bulk_insert do
owner = OwnerModel.new(my_models: array_of_my_model_instances)
# saves `my_models` using a single bulk insert (possibly via multiple batches)
owner.save!
end
このブロック内でBulkInsertSafe
、save
、ブロック外から呼び出したものとして扱われます。
既知の制限
これらのAPIの使用方法にはいくつかの制限があります:
-
BulkInsertableAssociations
:- 現在のところ、
has_many
リレーションシップにのみ対応しています。 -
has_many through: ...
関係にはまだ対応していません。
- 現在のところ、
さらに、入力データは最大でも1000レコード程度に制限されるか、バルクインサートを呼び出す前に既にバッチ化されている必要があります。INSERT
ステートメントは単一のトランザクションで実行されるため、大量のレコードの場合、データベースの安定性に悪影響を及ぼす可能性があります。