テーブルへの一括挿入
一度に大量のレコードを保存する必要があることもありますが、コレクションを反復して各レコードを個別に保存するのは非効率的です。 Rails 6 でinsert_all
が登場し、行レベルでオペレーションを行うようになったため (つまりHash
オブジェクトを使うようになったため)、GitLab ではActiveRecord
オブジェクトを安全かつ簡単に一括挿入できる API セットを追加しました。
一括挿入のためのApplicationRecord
s の準備
モデルクラスが一括挿入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
ある existing_model
場合、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
発生する s の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
、トランザクションをイシューすることになり、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
save
このブロックのBulkInsertSafe
でないリレーションを保存することはできます。
既知の制限
これらのAPIの使用方法にはいくつかの制限があります:
-
BulkInsertableAssociations
:- 現在のところ、
has_many
関係にのみ対応しています。 -
has_many through: ...
関係にはまだ対応していません。
- 現在のところ、
さらに、入力データは最大でも1000レコード程度に制限するか、バルクインサートを呼び出す前にすでにバッチ化されている必要があります。INSERT
ステートメントは単一のトランザクションで実行されるため、大量のレコードを使用するとデータベースの安定性に悪影響を及ぼす可能性があります。