批量插入表
有时候需要一次性存储大量记录,当遍历集合并逐个保存每条记录时,这可能会很低效。随着 Rails 6 中 insert_all 的到来(它在行级别操作,即使用 Hash 对象),GitLab 添加了一系列 API,使批量插入 ActiveRecord 对象变得安全和简单。
为批量插入准备 ApplicationRecord 模型
为了让模型类能够利用批量插入 API,它必须首先包含 BulkInsertSafe concern:
class MyModel < ApplicationRecord
# other includes here
# ...
include BulkInsertSafe # include this last
# ...
endBulkInsertSafe 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!。
你可能尝试插入的某些记录已经存在,这会导致主键冲突。解决这个问题有两种方法:快速失败(抛出错误)或跳过重复记录。bulk_insert! 的默认行为是快速失败并抛出 ActiveRecord::RecordNotUnique 错误。
如果这种行为不理想,你可以使用 skip_duplicates 标志来跳过重复记录:
MyModel.bulk_insert!(records, skip_duplicates: true)安全批量插入的要求
ActiveRecord 持久化 API 的很大一部分都围绕回调的概念构建。许多这些回调会在响应模型生命周期事件(如 save 或 create)时触发。这些回调不能用于批量插入,因为它们应该为每个被保存或创建的实例调用。由于在批量插入记录时这些事件不会触发,我们阻止了它们的使用。
哪些回调被明确允许的具体细节在 BulkInsertSafe 中定义。请查阅模块源代码以获取详细信息。如果你的类使用了未被明确标记为安全的回调,并且你 include BulkInsertSafe,应用程序将因错误而失败。
BulkInsertSafe 与 InsertAll 的比较
在内部,BulkInsertSafe 基于 InsertAll,你可能想知道何时选择前者而不是后者。为了帮助你做出决定,下表列出了这些类之间的主要区别。
| Input type | Validates input | Specify batch size | Can bypass callbacks | Transactional | |
|---|---|---|---|---|---|
bulk_insert! |
ActiveRecord objects | Yes (optional) | Yes (optional) | No (prevents unsafe callback use) | Yes |
insert_all! |
Attribute hashes | No | No | Yes | Yes |
总结来说,BulkInsertSafe 使批量插入更接近 ActiveRecord 对象和插入通常的行为方式。然而,如果你只需要批量插入原始数据,那么 insert_all 更高效。
批量插入 has_many 关联
一个常见的用例是通过关系的主端保存关联关系的集合,其中被拥有的关系通过 has_many 类方法与主端关联:
owner = OwnerModel.new(owned_relations: array_of_owned_relations)
# saves all `owned_relations` one-by-one
owner.save!这为 owned_relations 中的每条记录都发出一个 INSERT 和事务,如果 array_of_owned_relations 很大,这会很低效。为了解决这个问题,可以使用 BulkInsertableAssociations concern 来声明主端定义了适合批量插入的关联:
class OwnerModel < ApplicationRecord
# other includes here
# ...
include BulkInsertableAssociations # include this last
has_many :my_models
end这里 my_models 必须被声明为 BulkInsertSafe(如前所述)才能进行批量插入。你现在可以插入任何尚未保存的记录,如下所示:
BulkInsertableAssociations.with_bulk_insert do
owner = OwnerModel.new(my_models: array_of_my_model_instances)
# saves `my_models` using a single bulk insert (possibly via multiple batches)
owner.save!
end你仍然可以在这个块中保存不是 BulkInsertSafe 的关联;它们被当作你从块外调用 save 一样处理。
已知限制
这些 API 的使用有一些限制:
BulkInsertableAssociations:- 目前只兼容
has_many关系。 - 尚不支持
has_many through: ...关系。
- 目前只兼容
此外,输入数据应该限制在最多约 1000 条记录,或者在调用批量插入之前已经分批处理。INSERT 语句在单个事务中运行,因此对于大量记录,它可能会对数据库稳定性产生负面影响。