在Rails的一次调用中保存多个对象


93

我在Rails中有一种方法正在执行以下操作:

a = Foo.new("bar")
a.save

b = Foo.new("baz")
b.save

...
x = Foo.new("123", :parent_id => a.id)
x.save

...
z = Foo.new("zxy", :parent_id => b.id)
z.save

问题在于,我添加的实体越多,所需的时间就越长。我怀疑这是因为它必须为每个记录命中数据库。由于它们是嵌套的,所以我知道在保存父母之前我无法保存孩子,但是我想一次保存所有父母,然后保存所有孩子。做这样的事情会很好:

a = Foo.new("bar")
b = Foo.new("baz")
...
saveall(a,b,...)

x = Foo.new("123", :parent_id => a.id)
...
z = Foo.new("zxy", :parent_id => b.id)
saveall(x,...,z)

只需两个数据库命中就可以完成所有操作。有没有一种简便的方法可以在Rails中执行此操作,或者我一次只能执行一次?

Answers:


69

您可以尝试使用Foo.create代替Foo.new。创建“如果通过验证,则创建一个(或多个)对象并将其保存到数据库。无论对象是否成功保存到数据库,都将返回生成的对象。”

您可以创建多个对象,如下所示:

# Create an Array of new objects
  parents = Foo.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])

然后,对于每个父级,还可以使用create添加到其关联中:

parents.each do |parent|
  parent.children.create (:child_name => 'abc')
end

我建议您同时阅读ActiveRecord文档和有关ActiveRecord查询界面ActiveRecord关联的Rails指南。后者包含了类在声明关联时获得的所有方法的指南。


78
不幸的是,ActiveRecord将为每个创建的模型生成一个INSERT查询。OP需要一个INSERT调用,而ActiveRecord则不会。
弗朗索瓦博索莱伊

是的,我希望在一次插入调用中获得全部信息,但是如果activerecord不是那么聪明,我想这并不是一件容易的事。
captncraig

@FrançoisBeausoleil您介意查看问题stackoverflow.com/questions/15386450 / ...,这就是为什么我不能同时插入多个记录的原因吗?
Richlewis13年

3
的确,您不能让AR生成一个INSERT或UPDATE,但是使用ActiveRecord::Base.transaction { records.each(&:save) }或类似的方法,您至少可以将所有INSERT或UPDATE放入一个事务中。
yuval's

1
实际上,OP希望减少对数据库的访问,以加快数据库访问速度,而ActiveRecord实际上可以让您通过在一个事务中分批处理所有调用来做到这一点。(请参阅Harish的答案,这应该是公认的答案。)ActiveRecord不会让您做的是让数据库为每个事务创建一个INSERT查询,但这没关系,因为延迟是通过网络进行的。访问数据库,而不是在数据库内部执行INSERT查询时访问数据库。
Magne

99

由于您需要执行多次插入,因此数据库将被多次击中。这种情况下的延迟是因为每次保存都是在不同的数据库事务中完成的。您可以通过将所有操作包含在一个事务中来减少延迟。

class Foo
  belongs_to  :parent,   :class_name => "Foo"
  has_many    :children, :class_name => "Foo", :foreign_key=> "parent_id"
end

您的保存方法可能如下所示:

# build the parent and the children
a = Foo.new(:name => "bar")
a.children.build(:name => "123")

b = Foo.new("baz")
b.children.build(:name => "zxy")

#save parents and their children in one transaction
Foo.transaction do
  a.save!
  b.save!
end

save对父对象的调用将保存子对象。


3
正是我想要的。大大加快了我的种子速度。谢谢:-)
Renra 2014年

16

insert_all(Rails 6+)

Rails 6引入了一个新方法insert_all,该方法可在一条SQL INSERT语句中将多个记录插入数据库。

另外,此方法不实例化任何模型,也不调用Active Record回调或验证。

所以,

Foo.insert_all([
  { first_name: 'Jamie' },
  { first_name: 'Jeremy' }
])

它比

Foo.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])

如果您要做的就是插入新记录。


1
我等不及要更新我们的应用程序。Rails中6.因此,许多很酷的事情

1
需要注意的一点是:insert_all跳过AR回调和验证:edgeguides.rubyonrails.org/...
sujay

11

在其他地方找到的两个答案之一:比林顿(Beerlington)。这两个是您表现的最佳选择


我认为您最好的选择是使用SQL,并在每个查询中批量插入多行。如果您可以构建一个类似以下内容的INSERT语句:

插入foos_bars(foo_id,bar_id)值(1,1),(1,2),(1,3)...。您应该能够在单个查询中插入数千行。我没有尝试过您的mass_habtm方法,但似乎可以执行以下操作:


bars = Bar.find_all_by_some_attribute(:a) 
foo = Foo.create
values = bars.map {|bar| "(#{foo.id},#{bar.id})"}.join(",") 
connection.execute("INSERT INTO foos_bars (foo_id, bar_id) VALUES
#{values}")

另外,如果要通过“ some_attribute”搜索Bar,请确保在数据库中索引了该字段。


要么

您仍然可以看看activerecord-import。不错,没有模型就无法使用,但是您可以为导入创建模型。


FooBar.import [:foo_id, :bar_id], [[1,2], [1,3]]

干杯


这对于插入非常有用,但是在一个事务中更新多个记录又如何呢?
Avishai 2013年

2
要进行更新,您应该使用upsert:github.com/seamusabshere/upsert。欢呼声
Nguyen Chien Cong

sql查询的想法很差。您应该使用ActiveRecord和事务。
Kerozu 2014年

这不是一个坏主意。我想,如果您要进行一次插入,它将成功或失败,不需要事务。或者,您始终可以将一个插入内容包装在事务块中。
Fernando Fabreti 2014年

这是错误的做法
Blair Anderson

0

您需要使用此宝石“ FastInserter”-> https://github.com/joinhandshake/fast_inserter

并且插入大量和数千条记录的速度很快,因为此gem会跳过活动记录,并且仅使用单个sql原始查询


1
尽管指向gem的链接可能有用,但是请提供Asker可以使用的一些代码,而不是其当前代码(请参阅问题)。
燕窝


1
答案需要嵌入必要的信息。请编辑您的答案,然后在其中添加链接,并在答案中添加它的基本部分,以便使其独立。
特里科特

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.