Speichern mehrerer Objekte in einem einzigen Aufruf in Schienen

90

Ich habe eine Methode in Schienen, die ungefähr so ​​funktioniert:

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

Das Problem ist, dass dies immer länger dauert, je mehr Entitäten ich hinzufüge. Ich vermute, das liegt daran, dass es für jeden Datensatz die Datenbank treffen muss. Da sie verschachtelt sind, weiß ich, dass ich die Kinder nicht retten kann, bevor die Eltern gerettet sind, aber ich möchte alle Eltern auf einmal und dann alle Kinder auf einmal retten. Es wäre schön, so etwas zu tun:

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)

Das würde alles in nur zwei Datenbanktreffern erledigen. Gibt es eine einfache Möglichkeit, dies in Schienen zu tun, oder bin ich festgefahren, es einzeln zu tun?

captncraig
quelle

Antworten:

64

Sie können versuchen, Foo.create anstelle von Foo.new zu verwenden. Erstellen "Erstellt ein Objekt (oder mehrere Objekte) und speichert es in der Datenbank, wenn die Validierungen erfolgreich sind. Das resultierende Objekt wird zurückgegeben, unabhängig davon, ob das Objekt erfolgreich in der Datenbank gespeichert wurde oder nicht."

Sie können mehrere Objekte wie folgt erstellen:

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

Anschließend können Sie für jedes übergeordnete Element auch create verwenden, um seine Zuordnung hinzuzufügen:

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

Ich empfehle, sowohl die ActiveRecord-Dokumentation als auch die Rails Guides zur ActiveRecord-Abfrageschnittstelle und die ActiveRecord-Zuordnungen zu lesen . Letzteres enthält eine Anleitung zu allen Methoden, die eine Klasse erhält, wenn Sie eine Zuordnung deklarieren.

Roadmaster
quelle
76
Leider generiert ActiveRecord eine INSERT-Abfrage pro erstelltem Modell. Das OP möchte einen einzelnen INSERT-Aufruf, den ActiveRecord nicht ausführt.
François Beausoleil
Ja, ich hatte gehofft, alles in einem Insert-Aufruf zu bekommen, aber wenn Activerecord nicht so klug ist, ist es wohl nicht sehr einfach.
Captncraig
@ FrançoisBeausoleil Würde es Ihnen etwas ausmachen , die Frage stackoverflow.com/questions/15386450/… zu betrachten? Wäre dies der Grund, warum ich nicht mehrere Datensätze gleichzeitig einfügen kann?
Richlewis
3
Es ist wahr, dass Sie AR nicht dazu bringen können, ein INSERT oder UPDATE zu generieren, aber mit ActiveRecord::Base.transaction { records.each(&:save) }oder ähnlich können Sie zumindest alle INSERTs oder UPDATEs in einer einzigen Transaktion zusammenfassen.
Yuval
1
Tatsächlich möchte das OP weniger auf die Datenbank zugreifen, um den DB-Zugriff zu beschleunigen, und ActiveRecord lässt Sie dies tatsächlich tun, indem alle Aufrufe in einer Transaktion zusammengefasst werden. (Siehe Harishs Antwort, die die akzeptierte Antwort sein sollte.) ActiveRecord lässt Sie nicht zu, dass die Datenbank eine INSERT-Abfrage pro Transaktion erstellt. Dies ist jedoch nicht so wichtig, da die Latenz vom Netzwerk ausgeht Zugriff auf die Datenbank und nicht innerhalb der Datenbank selbst, wenn die INSERT-Abfragen ausgeführt werden.
Magne
97

Da Sie mehrere Einfügungen durchführen müssen, wird die Datenbank mehrmals aufgerufen. Die Verzögerung in Ihrem Fall ist darauf zurückzuführen, dass jedes Speichern in verschiedenen DB-Transaktionen erfolgt. Sie können die Latenz reduzieren, indem Sie alle Ihre Vorgänge in einer Transaktion zusammenfassen.

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

Ihre Speichermethode könnte folgendermaßen aussehen:

# 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

Der saveAufruf des übergeordneten Objekts speichert die untergeordneten Objekte.

Harish Shetty
quelle
3
Genau das, wonach ich gesucht habe. Beschleunigt meine Samen sehr. Danke :-)
Renra
11

insert_all (Rails 6+)

Rails 6führte eine neue Methode insert_all ein , die mehrere Datensätze in einer einzigen SQL INSERTAnweisung in die Datenbank einfügt .

Außerdem instanziiert diese Methode keine Modelle und ruft keine Active Record-Rückrufe oder -Validierungen auf.

So,

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

es ist deutlich effizienter als

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

Wenn Sie nur neue Datensätze einfügen möchten.

Marian13
quelle
Ich kann es kaum erwarten, bis wir unsere App aktualisieren. So viele coole Dinge in Rails 6.
Dan
10

Eine der beiden Antworten, die woanders gefunden wurden: von Beerlington . Diese beiden sind die beste Wahl für die Leistung


Ich denke, Ihre beste Leistung wird darin bestehen, SQL zu verwenden und mehrere Zeilen pro Abfrage in großen Mengen einzufügen. Wenn Sie eine INSERT-Anweisung erstellen können, die Folgendes bewirkt:

INSERT INTO foos_bars (foo_id, bar_id) VALUES (1,1), (1,2), (1,3) .... Sie sollten in der Lage sein, Tausende von Zeilen in eine einzelne Abfrage einzufügen. Ich habe Ihre mass_habtm-Methode nicht ausprobiert, aber es scheint, als könnten Sie etwas tun wie:


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

Wenn Sie die Leiste nach "some_attribute" durchsuchen, stellen Sie außerdem sicher, dass dieses Feld in Ihrer Datenbank indiziert ist.


ODER

Vielleicht sehen Sie sich noch Activerecord-Import an. Es ist richtig, dass es ohne Modell nicht funktioniert, aber Sie können ein Modell nur für den Import erstellen.


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

Prost

Nguyen Chien Cong
quelle
Das funktioniert gut zum Einfügen, aber was ist mit dem Aktualisieren mehrerer Datensätze in einer Transaktion?
Avishai
2
Zum Aktualisieren sollten Sie upsert verwenden: github.com/seamusabshere/upsert . Prost
Nguyen Chien Cong
Sehr schlechte Idee mit SQL-Abfrage. Sie sollten ActiveRecord und Transaktion verwenden.
Kerozu
Das ist keine schlechte Idee. Wenn Sie EINE Einfügung durchführen, wird dies entweder erfolgreich sein oder fehlschlagen, ich denke, es ist keine Transaktion erforderlich. Oder Sie können diese EINE Einfügung jederzeit in einen Transaktionsblock einschließen.
Fernando Fabreti
Dies ist eine schlechte Schienenübung
Blair Anderson
1

Sie müssen dieses Juwel "FastInserter" verwenden -> https://github.com/joinhandshake/fast_inserter

Das Einfügen einer großen Anzahl und Tausender von Datensätzen ist schnell, da dieses Juwel den aktiven Datensatz überspringt und nur eine einzige SQL-Rohabfrage verwendet

val caro
quelle
1
Obwohl der Link zum Edelstein nützlich sein kann, geben Sie bitte einen Code an, den der Asker anstelle seines aktuellen Codes verwenden könnte (siehe Frage).
Trincot
1
Bei Antworten müssen die wesentlichen Informationen eingebettet sein . Bitte bearbeiten Sie Ihre Antwort und fügen Sie dort den Link hinzu. Fügen Sie auch die wesentlichen Teile davon in die Antwort ein, damit sie in sich geschlossen ist.
Trincot
0

Sie brauchen keinen Edelstein, um DB schnell und nur einmal zu treffen!

Jackrg hat es für uns ausgearbeitet: https://gist.github.com/jackrg/76ade1724bd816292e4e

Fernando Fabreti
quelle
Gibt es eine solche Lösung für Mongodb?
Breno