テーブルへの一括挿入

一度に大量のレコードを保存する必要があることもありますが、コレクションを反復して各レコードを個別に保存するのは非効率的です。 Rails 6 でinsert_allが登場し、行レベルでオペレーションを行うようになったため (つまりHash オブジェクトを使うようになったため)、GitLab ではActiveRecord オブジェクトを安全かつ簡単に一括挿入できる API セットを追加しました。

一括挿入のためのApplicationRecords の準備

モデルクラスが一括挿入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の大部分は、コールバックの概念を中心に構築されています。これらのコールバックの多くは、savecreateのようなモデルのライフサイクルイベントに応答して起動します。これらのコールバックは、保存または作成されるインスタンスごとに呼び出されることを意図しているため、一括挿入では使用できません。 これらのイベントは、レコードが一括挿入されるときには起動しないため、現在、これらの使用を許可していません。

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

BulkInsertSafeInsertAll

内部的には、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 ステートメントは単一のトランザクションで実行されるため、大量のレコードを使用するとデータベースの安定性に悪影響を及ぼす可能性があります。