有些情况下,我们想批量插入记录,例如,我们有一个CSV的用户列表,我们想在我们的应用程序中导入这些用户。
Rails有一些方法,如delete_all 或update_all ,分别用于删除和更新批量记录。 但缺少一个类似的方法来插入批量记录。
Rails 6添加了insert_all 、insert_all! 和upsert_all 到ActiveRecord::Persistence ,以解决上述问题。
在Rails 6之前
在Rails 6之前,批量插入是通过以下方式实现的
- 使用activerecord-importgem
users = []
10.times do |i|
users << User.new(name: "user #{i}")
end
User.import users
- 一条一条地创建记录
10.times do |i|
User.create!(name: "user #{i}")
end
- 使用SQL的INSERT语句
# Assuming users is an array of user hash
# like [{ name: "Sam" }, { name: "Charls" }]
sql = "INSERT INTO users VALUES "
sql_values = []
users.each do |user|
sql_values << "(#{user.values.join(", ")})"
end
sql += sql_values.join(", ")
ActiveRecord::Base.connection.insert_sql(sql)
在Rails 6中
insert_all和insert_all!
使用insert_all ,我们可以执行批量插入,如下------。
result = User.insert_all(
[
{
name: "Sam",
email: "sam@example.com"
},
{
name: "Charls",
email: "charls@example.com"
}
]
)
# Bulk Insert (2.3ms) INSERT INTO "users"("name","email")
# VALUES("Sam", "sam@example"...)
# ON CONFLICT DO NOTHING RETURNING "id"
puts result.inspect
#<ActiveRecord::Result:0x00007fb6612a1ad8 @columns=["id"], @rows=[[1], [2]],
@hash_rows=nil, @column_types=
{"id"=>#<ActiveModel::Type::Integer:0x00007fb65f420078 ....>
puts User.count
=> 2
如上所述,我们注意到查询中的ON CONFLICT DO NOTHING 子句。SQLite和PostgreSQL数据库都支持这一点。 如果在批量插入过程中出现了冲突或唯一键的违反,它会跳过冲突的记录,继续插入下一条记录。
如果我们需要确保所有的记录都被插入,我们可以直接使用insert_all!,bang版本。
RETURNING "id" 上述查询中的子句返回主键 。如果我们想检查除 以外的更多属性,我们可以使用一个可选的 选项,它期望一个属性名称的数组。@rows=[[1], [2]] id returning
result = User.insert_all(
[
{
name: "Sam",
email: "sam@example.com"
},
{
name: "Charls",
email: "charls@example.com"
}
],
returning: %w[id name]
)
# Bulk Insert (2.3ms) INSERT INTO "users"("name","email")
# VALUES("Sam", "sam@example"...)
# ON CONFLICT DO NOTHING RETURNING "id", "name"
puts result.inspect
#<ActiveRecord::Result:0x00007fb6612a1ad8 @columns=["id", "name"],
@rows=[[1, "Sam"], [2, "Charls"]],
@hash_rows=nil, @column_types=
{"id"=>#<ActiveModel::Type::Integer:0x00007fb65f420078 ....>
upsert_all
insert_all 和insert_all! ,如果在批量插入时遇到重复的记录,要么跳过重复的记录,要么引发一个异常。
如果一个记录存在,我们要更新它,否则就创建一个新的记录。 这就是所谓的upsert。
upsert_all 方法可以执行批量插入。
result = User.upsert_all(
[
{
id: 1,
name: "Sam new",
email: "sam@example.com"
},
{
id: 1, # duplicate id here
name: "Sam's new",
email: "sam@example.com"
},
{
id: 2,
name: "Charles", # name updated
email: "charls@example.com"
},
{
id: 3, # new entry
name: "David",
email: "david@example.com"
}
]
)
# Bulk Insert (26.3ms) INSERT INTO `users`(`id`,`name`,`email`)
# VALUES (1, 'Sam new', 'sam@example.com')...
# ON DUPLICATE KEY UPDATE `name`=VALUES(`name`)
puts User.count
=> 3
输入数组中的第二行有重复的id 1,因此用户的名字将是Sam's new ,而不是Sam new 。
输入数组中的第三行没有重复,它将执行一个更新。
第四行的id 3是一个新条目,因此在这里将执行插入操作。
注意: upsert_all 的行为对不同的数据库是不同的。上述例子对MySQL 数据库有效,但对SQLite 则无效。
按顺序插入或更新记录有很大的性能问题。
让我们尝试顺序插入1000个用户,并对其进行基准测试。
print Benchmark.measure { 1000.times {|t| User.create(name: "name - #{t}")} }
7.913459 1.129483 9.439012 ( 15.329382 )
=> nil
在1000个事务中,执行1000个顺序插入查询,在15.32秒内创建了1000个本地用户。
使用insert_all ,我们可以准备用户的数据集,并在一个查询中导入它们。
users = 1000.times.map { |t| { name: "name - #{t}", created_at: Time.now, updated_at: Time.now }}
print Benchmark.measure { User.insert_all(users) }
0.267381 0.018721 0.298123 ( 0.401876 )
正如所见,导入1000个用户的时间从15.32秒减少到0.40秒。