테이블 일괄 삽입

가끔은 한꺼번에 대량의 레코드를 저장해야 하는 경우가 있는데, 컬렉션을 반복하고 각 레코드를 개별적으로 저장하는 것은 효율적이지 않을 수 있습니다. 레일즈 6에서 insert_all이 도입되면서 GitLab은 Hash 객체를 사용하여 동작하는 이러한 행 수준의 기능을 추가하여 ActiveRecord 객체를 대량으로 삽입할 수 있도록 하는 일련의 API를 추가했습니다.

대량 삽입을 위한 ApplicationRecord 준비

모델 클래스가 대량 삽입 API를 활용하려면 먼저 BulkInsertSafe 관심사를 포함해야 합니다.

class MyModel < ApplicationRecord
  # 다른 포함 항목들
  # ...
  include BulkInsertSafe # 마지막에 이것을 포함하세요

  # ...
end

BulkInsertSafe 관심사에는 두 가지 기능이 있습니다.

  • 대량 삽입에 대해 안전하지 않은 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의 크기 및 비용 사이에는 트레이드오프가 존재합니다.

중복 레코드 처리

참고: 이 매개변수는 bulk_insert!에만 적용됩니다. 기존 레코드를 업데이트하려는 경우 bulk_upsert!를 사용하세요.

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

원치 않다면 skip_duplicates 플래그를 사용하여 대신 중복 레코드를 건너뛸 수 있습니다.

MyModel.bulk_insert!(records, skip_duplicates: true)

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

ActiveRecord의 지속성 API의 많은 부분은 콜백 개념을 중심으로 구축되어 있습니다. 이러한 콜백 중 많은 것들이 save 또는 create와 같은 모델 라이프사이클 이벤트에 응답하여 발생합니다. 이러한 콜백은 대량 삽입에 사용할 수 없으며, 그 이유는 이러한 콜백이 저장되거나 생성되었을 때마다 호출되어야 하는 것이기 때문입니다. 레코드가 대량으로 삽입될 때 이러한 이벤트들이 발생하지 않으므로 현재 그 사용을 방지하고 있습니다.

명시적으로 허용된 콜백에 대한 구체적인 내용은 BulkInsertSafe에 정의되어 있습니다. 클래스가 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 concern을 사용하여 소유자가 대량 삽입에 대해 안전한 관계를 정의한다고 선언할 수 있습니다:

class OwnerModel < ApplicationRecord
  # 다른 include 내용들
  # ...
  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문은 단일 트랜잭션에서 실행되므로 대량의 레코드의 경우 데이터베이스 안정성에 부정적으로 작용할 수 있습니다.