테이블에 배치로 삽입하기

때때로 많은 양의 레코드를 한 번에 저장해야 할 필요가 있으며, 이는 컬렉션을 반복하고 각 레코드를 개별적으로 저장할 때 비효율적일 수 있습니다. Rails 6에서 도입된 insert_all 기능은 행 수준에서 작동하며(즉, Hash 객체를 사용), GitLab은 ActiveRecord 객체를 안전하고 간단하게 대량으로 삽입할 수 있는 API 세트를 추가했습니다.

대량 삽입을 위한 ApplicationRecord 준비

모델 클래스가 대량 삽입 API를 사용하려면 먼저 BulkInsertSafe concern을 포함해야 합니다:

class MyModel < ApplicationRecord
  # 다른 포함들
  # ...
  include BulkInsertSafe # 마지막에 포함

  # ...
end

BulkInsertSafe concern은 두 가지 기능을 가지고 있습니다:

  • 대량 삽입과 관련하여 안전하지 않은 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_modelname 속성이 있고, 동일한 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개의 레코드를 삽입하면 두 개의 배치가 순차적으로 기록됩니다(각각 500과 450 크기).

:batch_size 옵션을 통해 기본 배치 크기를 재정의할 수 있습니다:

MyModel.bulk_insert!(records, batch_size: 100)

950개의 레코드를 삽입할 때, 이는 대신 10개의 배치가 기록되도록 합니다. 이 또한 발생하는 INSERT 문 수에 영향을 미치므로, 이로 인해 코드에 미칠 성능 영향을 측정하는 것이 중요합니다. 데이터베이스가 처리해야 하는 INSERT 문 수와 각 INSERT의 크기 및 비용 사이에는 균형이 필요합니다.

중복 레코드 처리

note
이 매개변수는 bulk_insert!에만 적용됩니다. 기존 레코드를 업데이트하려면 대신 bulk_upsert!를 사용하세요.

삽입하려는 일부 레코드가 이미 존재할 수 있으며, 이로 인해 기본 키 충돌이 발생할 수 있습니다. 이 문제를 해결하는 방법은 두 가지가 있습니다: 오류를 발생시켜 빠르게 실패하거나 중복 레코드를 건너뛰는 것입니다. bulk_insert!의 기본 동작은 빠르게 실패하고 ActiveRecord::RecordNotUnique 오류를 발생시키는 것입니다.

이것이 바람직하지 않다면, 대신 skip_duplicates 플래그를 사용하여 중복 레코드를 건너뛸 수 있습니다:

MyModel.bulk_insert!(records, skip_duplicates: true)

안전한 대량 삽입을 위한 요구 사항

ActiveRecord의 지속성 API의 많은 부분은 콜백 개념에 기반하여 구축되어 있습니다. 이러한 콜백의 대부분은 save 또는 create와 같은 모델 생명 주기 이벤트에 반응하여 발생합니다. 이러한 콜백은 모든 인스턴스가 저장되거나 생성될 때 호출될 수 있도록 설계되었기 때문에 대량 삽입에서는 사용할 수 없습니다. 레코드가 대량으로 삽입될 때 이러한 이벤트가 발생하지 않기 때문에 사용을 방지합니다.

명시적으로 허용된 콜백에 대한 구체적인 내용은 BulkInsertSafe에서 정의되어 있습니다. 자세한 내용은 모듈 소스 코드를 참조하세요. 클래스가 명시적으로 안전하지 않은 콜백을 사용하는 경우, include BulkInsertSafe를 지정하면 애플리케이션이 오류로 실패합니다.

BulkInsertSafeInsertAll

내부적으로 BulkInsertSafeInsertAll에 기반하고 있으며, 이전자를 선택해야 하는 경우 궁금할 수 있습니다. 결정을 내리는 데 도움을 주기 위해, 이 두 클래스 간의 주요 차이점은 아래의 표에 나열되어 있습니다.

  입력 유형 입력 유효성 검사 배치 크기 지정 콜백 우회 가능 트랜잭셔널
bulk_insert! ActiveRecord 객체 예 (선택적) 예 (선택적) 아니오 (안전하지 않은 콜백 사용 방지)
insert_all! 속성 해시 아니오 아니오

요약하자면, BulkInsertSafe는 대량 삽입을 ActiveRecord 객체와 일반 삽입 방식에 더 가깝게 이동시킵니다. 그러나 원시 데이터를 대량으로 삽입해야 하는 경우, insert_all이 더 효율적입니다.

has_many 연관을 대량으로 삽입하기

일반적인 사용 사례는 주인의 관점에서 연관된 관계의 컬렉션을 저장하는 것입니다. 여기서 소유 연관은 has_many 클래스 메서드를 통해 주인과 연결됩니다:

owner = OwnerModel.new(owned_relations: array_of_owned_relations)
# 모든 `owned_relations`를 하나씩 저장합니다.
owner.save!

이는 owned_relations의 각 레코드에 대해 단일 INSERT와 트랜잭션을 발행하는데, 이는 array_of_owned_relations가 클 경우 비효율적입니다. 이를 해결하기 위해 BulkInsertableAssociations 컨선이 사용되어 주인이 대량 삽입에 안전한 연관을 정의해야 함을 선언할 수 있습니다:

class OwnerModel < ApplicationRecord
  # 기타 포함 사항
  # ...
  include BulkInsertableAssociations # 가장 마지막에 포함합니다.

  has_many :my_models
end

여기서 my_models는 대량 삽입이 발생하도록 BulkInsertSafe로 선언되어야 합니다. 이제 다음과 같이 아직 저장되지 않은 레코드를 삽입할 수 있습니다:

BulkInsertableAssociations.with_bulk_insert do
  owner = OwnerModel.new(my_models: array_of_my_model_instances)
  # `my_models`를 단일 대량 삽입을 통해 저장합니다 (가능성 있는 여러 배치를 통해)
  owner.save!
end

여전히 이 블록 내에서 BulkInsertSafe가 아닌 관계를 저장할 수 있습니다. 이 경우, 블록 밖에서 save를 호출한 것과 같이 처리됩니다.

알려진 제한 사항

이 API를 사용할 때 몇 가지 제약이 있습니다:

  • BulkInsertableAssociations:
    • 현재 has_many 관계와만 호환됩니다.
    • has_many through: ... 관계는 아직 지원되지 않습니다.

또한, 입력 데이터는 최대 1000레코드로 제한되거나, 일괄 삽입을 호출하기 전에 이미 배치되어야 합니다. INSERT 문은 단일 트랜잭션으로 실행되므로, 대량의 레코드에 대해서는 데이터베이스 안정성에 부정적인 영향을 미칠 수 있습니다.