대량 삽입을 위한 테이블에 삽입

가끔은 한꺼번에 대량의 레코드를 저장해야 할 때가 있는데, 컬렉션을 반복하고 각 레코드를 개별적으로 저장하는 것은 비효율적일 수 있습니다. Rails 6에서 insert_all이 도입되면서 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의 많은 부분은 콜백을 중심으로 구축되어 있습니다. 이러한 콜백의 많은 부분은 save 또는 create와 같은 모델 생명 주기 이벤트에 응답하여 발생합니다. 이러한 콜백은 모든 인스턴스에 대해 호출되도록 의도되었기 때문에 대량 삽입에 사용할 수 없습니다. 이벤트가 대량으로 삽입될 때 호출되지 않으므로 현재 이러한 사용을 방지하고 있습니다.

명시적으로 허용된 콜백에 대한 자세한 내용은 BulkInsertSafe에서 정의됩니다. 클래스가 안전하게 지정되지 않은 콜백을 사용하고 BulkInsertSafe를 포함하면 애플리케이션에서 오류가 발생합니다.

BulkInsertSafeInsertAll

내부적으로 BulkInsertSafeInsertAll에 기반하고 있으며, 언제 BulkInsertSafe를 선택해야 하는지 궁금할 수 있습니다. 이 결정을 내릴 수 있도록 이러한 클래스 사이의 주요 차이점을 아래 표에 나열했습니다.

  입력 유형 입력 유효성 검사 일괄 크기 지정 콜백 우회 가능 트랜잭션 처리
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!

이 방식은 array_of_owned_relations이 크기가 크다면, owned_relations의 각 레코드에 대해 단일 INSERT와 트랜잭션이 발생하여 비효율적일 수 있습니다. 이 문제를 해결하기 위해 BulkInsertableAssociations concern을 사용하여 소유자가 대량 삽입에 안전한 관계를 정의한다고 선언할 수 있습니다.

class OwnerModel < ApplicationRecord
  # 다른 포함 사항 여기에
  # ...
  include BulkInsertableAssociations # 마지막에 이를 포함시킵니다
  
  has_many :my_models
end

여기서 my_models는 이전에 설명한 대량 삽입에 안전하게 선언되어야 합니다. 이제 다음과 같이 아직 저장되지 않은 레코드를 삽입할 수 있습니다.

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 문은 단일 트랜잭션에서 실행되므로 많은 양의 레코드의 경우 데이터베이스 안정성에 부정적인 영향을 줄 수 있습니다.