如何在Rails 6批量插入记录

388 阅读2分钟

有些情况下,我们想批量插入记录,例如,我们有一个CSV的用户列表,我们想在我们的应用程序中导入这些用户。

Rails有一些方法,如delete_allupdate_all ,分别用于删除和更新批量记录。 但缺少一个类似的方法来插入批量记录。

Rails 6添加了insert_allinsert_all!upsert_allActiveRecord::Persistence ,以解决上述问题。

在Rails 6之前

在Rails 6之前,批量插入是通过以下方式实现的

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_allinsert_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秒。