- 대량 삽입을 위한
ApplicationRecord
준비 -
bulk_insert!
및bulk_upsert!
를 사용하여 레코드 삽입 has_many
연관 관계를 대량으로 삽입- 알려진 제한 사항
테이블 일괄 삽입
가끔은 한 번에 많은 양의 레코드를 저장해야 하는 경우가 있는데, 이는 컬렉션을 반복하고 각 레코드를 개별적으로 저장하는 것은 비효율적일 수 있습니다. 레일즈 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_model
에 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개의 레코드를 삽입하면 연속적으로 쓰여진 두 개의 배치가 발생합니다(각각 크기가 500과 450).
batch_size
옵션을 통해 기본 배치 크기를 재정의할 수 있습니다:
MyModel.bulk_insert!(records, batch_size: 100)
950개의 레코드를 대상으로 한 경우, 결과적으로 10개의 배치가 작성됩니다. 이것은 발생하는 INSERT
문의 수에도 영향을 미치기 때문에 코드에 미치는 성능 영향을 측정하는 것이 중요합니다. 데이터베이스가 처리해야 하는 INSERT
문의 수와 각 INSERT
의 크기 및 비용 사이에는 균형이 필요합니다.
중복 레코드 처리
참고:
이 매개변수는 bulk_insert!
에만 적용됩니다. 기존 레코드를 업데이트하려면 bulk_upsert!
를 사용하세요.
삽입하려는 일부 레코드가 이미 존재하기 때문에 기본 키 충돌이 발생할 수도 있습니다. 이 문제를 해결하는 방법으로 빠르게 실패시키고 ActiveRecord::RecordNotUnique
오류를 발생시키거나 중복 레코드를 건너뛰는 두 가지 방법이 있습니다. bulk_insert!
의 기본 동작은 빠르게 실패시키고 ActiveRecord::RecordNotUnique
오류를 발생시키는 것입니다.
원치 않다면, 대신 skip_duplicates
플래그로 중복 레코드를 건너뛸 수 있습니다:
MyModel.bulk_insert!(records, skip_duplicates: true)
안전한 대량 삽입을 위한 요구사항
ActiveRecord의 많은 부분은 콜백을 중심으로 구성되어 있습니다. 이러한 콜백 중 많은 것이 save
또는 create
와 같은 모델 생명 주기 이벤트에 대한 응답으로 발생합니다. 이러한 콜백은 대량 삽입에 사용할 수 없으며, 왜냐하면 이들은 각각이 저장되거나 생성되는 모든 인스턴스에 대해 호출되어야 하기 때문입니다. 대량 삽입시 이러한 이벤트가 발생하지 않기 때문에 이러한 콜백의 사용을 방지합니다.
명시적으로 허용되는 콜백에 대한 구체적인 내용은 BulkInsertSafe
에 정의되어 있습니다. 클래스가 안전하게 지정된 콜백을 사용하고 include BulkInsertSafe
를 사용한다면, 애플리케이션은 오류가 발생합니다.
BulkInsertSafe
대 InsertAll
내부적으로 BulkInsertSafe
는 InsertAll
을 기반으로 하며, 언제 BulkInsertSafe
대신 InsertAll
을 선택해야 하는지 궁금할 수 있습니다. 이 결정을 내리는 데 도움이 되도록, 이러한 클래스 간의 주요 차이점은 아래 표에 나열되어 있습니다.
입력 형식 | 입력 유효성 검사 | 배치 크기 지정 | 콜백 우회 가능 여부 | 트랜잭션 지원 여부 | |
---|---|---|---|---|---|
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
이 큰 경우 비효율적입니다. 이를 해결하기 위해, BulkInsertableAssociations
concern을 사용하여 소유자가 대량 삽입에 안전한 연관을 정의했음을 선언할 수 있습니다:
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
문은 단일 트랜잭션에서 실행되므로 많은 양의 레코드의 경우 데이터베이스 안정성에 부정적으로 작용할 수 있습니다.