테이블 일괄 삽입
가끔은 한꺼번에 대량의 레코드를 저장해야 하는 경우가 있는데, 컬렉션을 반복하고 각 레코드를 개별적으로 저장하는 것은 효율적이지 않을 수 있습니다. 레일즈 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_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!
를 사용하세요.
삽입하려는 일부 레코드가 이미 존재하는 경우 기본 키 충돌이 발생할 수 있습니다. 이 문제를 해결하는 두 가지 방법이 있습니다. 빠르게 실패를 감지하여 오류를 발생시키거나 중복 레코드를 건너뛰는 방법입니다. bulk_insert!
의 기본 동작은 빠르게 실패를 감지하여 ActiveRecord::RecordNotUnique
오류를 발생시키는 것입니다.
원치 않다면 skip_duplicates
플래그를 사용하여 대신 중복 레코드를 건너뛸 수 있습니다.
MyModel.bulk_insert!(records, skip_duplicates: true)
안전한 대량 삽입을 위한 요구 사항
ActiveRecord의 지속성 API의 많은 부분은 콜백 개념을 중심으로 구축되어 있습니다. 이러한 콜백 중 많은 것들이 save
또는 create
와 같은 모델 라이프사이클 이벤트에 응답하여 발생합니다. 이러한 콜백은 대량 삽입에 사용할 수 없으며, 그 이유는 이러한 콜백이 저장되거나 생성되었을 때마다 호출되어야 하는 것이기 때문입니다. 레코드가 대량으로 삽입될 때 이러한 이벤트들이 발생하지 않으므로 현재 그 사용을 방지하고 있습니다.
명시적으로 허용된 콜백에 대한 구체적인 내용은 BulkInsertSafe
에 정의되어 있습니다. 클래스가 BulkInsertSafe
를 포함하고 있지만 명시적으로 지정된 안전하지 않은 콜백을 사용하는 경우에 애플리케이션은 오류가 발생합니다.
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!
이 동작은 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
문은 단일 트랜잭션에서 실행되므로 대량의 레코드의 경우 데이터베이스 안정성에 부정적으로 작용할 수 있습니다.