Guardar varios objetos en una sola llamada en rieles

93

Tengo un método en rieles que está haciendo algo como esto:

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

El problema es que esto toma más tiempo y más entidades agrego. Sospecho que esto se debe a que tiene que llegar a la base de datos para cada registro. Como están anidados, sé que no puedo salvar a los niños antes de que se salven los padres, pero me gustaría salvar a todos los padres a la vez y luego a todos los niños. Sería bueno hacer algo como:

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)

Eso lo haría todo en solo dos visitas a la base de datos. ¿Existe una manera fácil de hacer esto en rieles, o estoy atrapado haciéndolo uno a la vez?

captncraig
fuente

Respuestas:

69

Puede intentar usar Foo.create en lugar de Foo.new. Crear "Crea un objeto (o varios objetos) y lo guarda en la base de datos, si pasan las validaciones. El objeto resultante se devuelve tanto si el objeto se guardó correctamente en la base de datos como si no".

Puede crear varios objetos como este:

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

Luego, para cada padre, también puede usar create para agregar a su asociación:

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

Recomiendo leer tanto la documentación de ActiveRecord como las guías de Rails sobre la interfaz de consulta de ActiveRecord y las asociaciones de ActiveRecord . Este último contiene una guía de todos los métodos que gana una clase cuando declaras una asociación.

Roadmaster
fuente
78
Desafortunadamente, ActiveRecord generará una consulta INSERT por modelo creado. El OP quiere una sola llamada INSERT, lo que ActiveRecord no hará.
François Beausoleil
Sí, esperaba tenerlo todo en una llamada de inserción, pero si activerecord no es tan inteligente, supongo que no es muy fácil.
captncraig
@ FrançoisBeausoleil, ¿le importaría mirar la pregunta stackoverflow.com/questions/15386450/… , por eso no puedo insertar varios registros al mismo tiempo?
Richlewis
3
Es cierto que no puede hacer que AR genere un INSERT o UPDATE, pero con ActiveRecord::Base.transaction { records.each(&:save) }o similar puede al menos poner todos los INSERT o UPDATE en una sola transacción.
Yuval
1
En realidad, el OP quiere llegar menos a la base de datos, para acelerar el acceso a la base de datos, y ActiveRecord realmente le permite hacerlo, agrupando todas las llamadas en una transacción. (Consulte la respuesta de Harish, que debería ser la respuesta aceptada). Lo que ActiveRecord no le permitirá hacer es hacer que la base de datos cree una consulta INSERT por transacción, pero eso no importa mucho, ya que la latencia proviene de la red acceso a la base de datos, y no dentro de la propia base de datos cuando realiza las consultas INSERT.
Magne
99

Dado que necesita realizar múltiples inserciones, la base de datos se activará varias veces. El retraso en su caso se debe a que cada guardado se realiza en diferentes transacciones de base de datos. Puede reducir la latencia al incluir todas sus operaciones en una sola transacción.

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

Su método de guardado podría verse así:

# 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

La savellamada al objeto principal guarda los objetos secundarios.

Harish Shetty
fuente
3
Justo lo que estaba buscando. Acelera mucho mis semillas. Gracias :-)
Renra
16

insert_all (rieles 6+)

Rails 6introdujo un nuevo método insert_all , que inserta varios registros en la base de datos en una sola SQL INSERTdeclaración.

Además, este método no crea una instancia de ningún modelo y no llama devoluciones de llamada o validaciones de Active Record.

Entonces,

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

es significativamente más eficiente que

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

si todo lo que quiere hacer es insertar nuevos registros.

Marian13
fuente
1
No puedo esperar hasta que actualicemos nuestra aplicación. Tantas cosas interesantes en Rails 6.
Dan
1
una cosa a tener en cuenta es: insert_all omite las devoluciones de llamada y validaciones de AR: edgeguides.rubyonrails.org/…
sujay
11

Una de las dos respuestas encontradas en otro lugar: por Beerlington . Esos dos son tu mejor apuesta para el rendimiento.


Creo que su mejor apuesta en cuanto al rendimiento será usar SQL e insertar de forma masiva varias filas por consulta. Si puede crear una declaración INSERT que haga algo como:

INSERT INTO foos_bars (foo_id, bar_id) VALUES (1,1), (1,2), (1,3) .... Debería poder insertar miles de filas en una sola consulta. No probé tu método mass_habtm, pero parece que podrías hacer algo como:


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}")

Además, si está buscando en Bar por "algún_ atributo", asegúrese de tener ese campo indexado en su base de datos.


O

Es posible que todavía eche un vistazo a activerecord-import. Es cierto que no funciona sin un modelo, pero puede crear un modelo solo para la importación.


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

Salud

Nguyen Chien Cong
fuente
Eso funciona muy bien para insertar, pero ¿qué pasa con la actualización de varios registros en una transacción?
Avishai
2
Para actualizar debe usar upsert: github.com/seamusabshere/upsert . aclamaciones
Nguyen Chien Cong
Muy mala idea con la consulta SQL. Debe utilizar ActiveRecord y la transacción.
Kerozu
No es mala idea. Si está haciendo UNA inserción, tendrá éxito o fallará, supongo que no es necesario realizar ninguna transacción. O siempre puede envolver esa UNIDAD insertada en un bloque de transacción.
Fernando Fabreti
esta es una mala práctica de rieles
Blair Anderson
0

necesitas usar esta gema "FastInserter" -> https://github.com/joinhandshake/fast_inserter

e insertar una gran cantidad y miles de registros es rápido porque esta gema omite el registro activo y solo usa una sola consulta SQL sin procesar

val caro
fuente
1
Aunque el enlace a la gema puede ser útil, proporcione algún código que el autor de la pregunta pueda usar en lugar del código actual (consulte la pregunta).
trincot
1
Las respuestas deben tener incorporada la información esencial . Edite su respuesta y agregue el enlace allí, y también agregue las partes esenciales dentro de la respuesta, para que sea independiente.
trincot