テーブルへの一括挿入

一度に大量のレコードを保存する必要があることがありますが、コレクションを繰り返し処理したり各レコードを個別に保存したりするのは非効率的です。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あります。

重複レコードの処理

note
このパラメータはbulk_insert! にのみ適用されます。 既存のレコードを更新する場合は、代わりにbulk_upsert! を使用してください。

挿入しようとしているレコードの中に既に存在するものがあり、主キーの競合が発生する可能性があります。この問題にアドレスするには、エラーを発生させて高速に失敗させる方法と、重複レコードをスキップする方法があります。bulk_insert! のデフォルトの動作は、高速に失敗してActiveRecord::RecordNotUnique エラーを発生させることです。

これが望ましくない場合は、skip_duplicates フラグで重複レコードをスキップすることができます:

MyModel.bulk_insert!(records, skip_duplicates: true)

安全な一括挿入の要件

ActiveRecordの永続化APIの大部分は、コールバックという概念を中心に構築されています。これらのコールバックの多くは、savecreate のようなモデルのライフサイクルイベントに応じて発生します。これらのコールバックは、保存または作成されるインスタンスごとに呼び出されることを想定しているため、一括挿入では使用できません。これらのイベントは、レコードが一括挿入されるときには発火しないため、現在のところ、これらの使用を禁止しています。

どのコールバックが明示的に許可されるかは、BulkInsertSafe で定義されています。詳細はモジュールのソース・コードを参照してください。クラスが明示的に安全であると指定されていないコールバックを使用し、include BulkInsertSafe 、アプリケーションはエラーで失敗します。

BulkInsertSafe 対してInsertAll

内部的には、BulkInsertSafeInsertAll をベースにしています。その決断を助けるために、これらのクラスの主な違いを以下の表に示します。

 入力タイプ入力の検証バッチサイズの指定コールバックをバイパス可能トランザクション
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

このブロック内でBulkInsertSafesave 、ブロック外から呼び出したものとして扱われます。

既知の制限

これらのAPIの使用方法にはいくつかの制限があります:

  • BulkInsertableAssociations:
    • 現在のところ、has_many リレーションシップにのみ対応しています。
    • has_many through: ... 関係にはまだ対応していません。

さらに、入力データは最大でも1000レコード程度に制限されるか、バルクインサートを呼び出す前に既にバッチ化されている必要があります。INSERT ステートメントは単一のトランザクションで実行されるため、大量のレコードの場合、データベースの安定性に悪影響を及ぼす可能性があります。