Zapisywanie wielu obiektów w jednym wywołaniu na szynach

93

Mam metodę w railsach, która robi coś takiego:

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

Problem polega na tym, że im więcej jednostek dodam, trwa to dłużej i dłużej. Podejrzewam, że dzieje się tak, ponieważ musi trafiać do bazy danych dla każdego rekordu. Ponieważ są zagnieżdżone, wiem, że nie mogę uratować dzieci, zanim rodzice zostaną uratowani, ale chciałbym uratować wszystkich rodziców naraz, a potem wszystkie dzieci. Byłoby miło zrobić coś takiego:

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)

To wystarczyłoby tylko na dwa trafienia w bazie danych. Czy jest łatwy sposób na zrobienie tego w szynach, czy też utknąłem, robiąc to pojedynczo?

captncraig
źródło

Odpowiedzi:

69

Możesz spróbować użyć Foo.create zamiast Foo.new. Create „Tworzy obiekt (lub wiele obiektów) i zapisuje go w bazie danych, jeśli sprawdzanie poprawności powiodło się. Wynikowy obiekt jest zwracany bez względu na to, czy obiekt został pomyślnie zapisany w bazie danych, czy nie”.

Możesz utworzyć wiele obiektów, takich jak ten:

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

Następnie dla każdego rodzica możesz również użyć funkcji create, aby dodać do powiązania:

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

Zalecam przeczytanie zarówno dokumentacji ActiveRecord, jak i przewodników po Railsach dotyczących interfejsu zapytań ActiveRecord i asocjacji ActiveRecord . Ta ostatnia zawiera przewodnik po wszystkich metodach, które klasa zyskuje po zadeklarowaniu asocjacji.

Roadmaster
źródło
78
Niestety, ActiveRecord wygeneruje jedno zapytanie INSERT na utworzony model. OP chce pojedynczego wywołania INSERT, którego ActiveRecord nie zrobi.
François Beausoleil
Tak, miałem nadzieję, że dostanę to wszystko w jednym wywołaniu wstawiania, ale jeśli activerecord nie jest tak sprytny, myślę, że nie jest to łatwe.
captncraig
@ FrançoisBeausoleil, czy mógłbyś spojrzeć na pytanie stackoverflow.com/questions/15386450/ ... czy to dlatego nie mogę wstawiać wielu rekordów w tym samym czasie?
Richlewis
3
To prawda, że ​​nie możesz zmusić AR do wygenerowania jednej INSERT lub UPDATE, ale za pomocą ActiveRecord::Base.transaction { records.each(&:save) }lub czegoś podobnego możesz przynajmniej umieścić wszystkie INSERT lub UPDATE w jednej transakcji.
yuval
1
W rzeczywistości OP chce mniej trafiać do bazy danych, aby przyspieszyć dostęp do bazy danych, a ActiveRecord faktycznie pozwala ci to zrobić, grupując wszystkie wywołania w jednej transakcji. (Zobacz odpowiedź Harisha, która powinna być zaakceptowaną odpowiedzią.) To, czego ActiveRecord nie pozwoli ci zrobić, to zmusić DB do utworzenia jednego zapytania INSERT na transakcję, ale to nie ma większego znaczenia, ponieważ opóźnienie pochodzi z pracy w sieci dostęp do bazy danych, a nie wewnątrz samej bazy danych, gdy wykonuje zapytania INSERT.
Magne
99

Ponieważ musisz wykonać wiele wstawień, baza danych zostanie trafiona wiele razy. Opóźnienie w twoim przypadku jest spowodowane tym, że każdy zapis jest wykonywany w różnych transakcjach DB. Opóźnienie można zmniejszyć, zamykając wszystkie operacje w jednej transakcji.

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

Twoja metoda zapisu może wyglądać następująco:

# 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

saveWezwanie do obiektu nadrzędnego zapisuje obiektów podrzędnych.

Harish Shetty
źródło
3
Właśnie tego szukałem. Bardzo przyspiesza moje nasiona. Dzięki :-)
Renra
16

insert_all (szyny 6+)

Rails 6wprowadził nową metodę insert_all , która wstawia wiele rekordów do bazy danych w jednej SQL INSERTinstrukcji.

Ponadto ta metoda nie tworzy instancji żadnych modeli i nie wywołuje wywołań zwrotnych ani walidacji modułu Active Record.

Więc,

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

jest znacznie wydajniejszy niż

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

jeśli wszystko, co chcesz zrobić, to wstawić nowe rekordy.

Marian13
źródło
1
Nie mogę się doczekać, aż zaktualizujemy naszą aplikację. Tyle fajnych rzeczy w Rails 6.
Dan
1
jedna rzecz do zapamiętania: insert_all pomija wywołania zwrotne i walidacje AR: edgeguides.rubyonrails.org/…
sujay
11

Jedna z dwóch odpowiedzi znalezionych gdzie indziej: przez Beerlington . Te dwa to najlepszy sposób na wydajność


Myślę, że najlepszym rozwiązaniem pod względem wydajności będzie użycie SQL i zbiorcze wstawianie wielu wierszy na zapytanie. Jeśli możesz zbudować instrukcję INSERT, która robi coś takiego:

INSERT INTO foos_bars (foo_id, bar_id) VALUES (1,1), (1,2), (1,3) .... Powinieneś być w stanie wstawić tysiące wierszy w jednym zapytaniu. Nie próbowałem twojej metody Mass_habtm, ale wygląda na to, że możesz coś takiego:


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

Ponadto, jeśli wyszukujesz Bar według "jakiegoś_atrybutu", upewnij się, że masz to pole zaindeksowane w swojej bazie danych.


LUB

Nadal możesz rzucić okiem na aktywny import nagrań. To prawda, że ​​nie działa bez modelu, ale możesz utworzyć Model tylko do importu.


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

Twoje zdrowie

Nguyen Chien Cong
źródło
To świetnie sprawdza się przy wstawianiu, ale co z aktualizacją wielu rekordów w jednej transakcji?
Avishai
2
Do aktualizacji należy użyć upsert: github.com/seamusabshere/upsert . okrzyki
Nguyen Chien Cong
Bardzo kiepski pomysł z zapytaniem sql. Powinieneś użyć ActiveRecord i transakcji.
Kerozu,
To nie jest zły pomysł. Jeśli robisz JEDEN wkład, to się powiedzie lub zawiedzie, myślę, że nie ma potrzeby transakcji. Lub zawsze możesz zawinąć tę JEDNĄ wkładkę w blok transakcji.
Fernando Fabreti
to jest zły trening
railsowy
0

musisz użyć tego klejnotu „FastInserter” -> https://github.com/joinhandshake/fast_inserter

a wstawianie dużej liczby i tysięcy rekordów jest szybkie, ponieważ ten klejnot pomija aktywny rekord i używa tylko jednego surowego zapytania sql

val caro
źródło
1
Chociaż link do klejnotu może być przydatny, prosimy o podanie kodu, którego Asker mógłby użyć zamiast swojego obecnego kodu (patrz pytanie).
trincot
1
Odpowiedzi muszą mieć osadzone podstawowe informacje . Edytuj swoją odpowiedź i dodaj tam link, a także dodaj najważniejsze jej części w odpowiedzi, aby była niezależna.
trincot