Menyimpan banyak objek dalam satu panggilan di rel

93

Saya memiliki metode di rel yang melakukan sesuatu seperti ini:

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

Masalahnya adalah ini membutuhkan waktu lebih lama dan lebih lama semakin banyak entitas yang saya tambahkan. Saya menduga ini karena harus mengenai database untuk setiap record. Karena mereka bersarang, saya tahu saya tidak bisa menyelamatkan anak-anak sebelum orang tua diselamatkan, tetapi saya ingin menyelamatkan semua orang tua sekaligus, dan kemudian semua anak-anak. Akan menyenangkan untuk melakukan sesuatu seperti:

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)

Itu akan melakukan semuanya hanya dalam dua klik database. Apakah ada cara mudah untuk melakukan ini di rel, atau apakah saya terjebak melakukannya satu per satu?

captncraig
sumber

Jawaban:

69

Anda dapat mencoba menggunakan Foo.create daripada Foo.new. Buat "Membuat satu objek (atau beberapa objek) dan menyimpannya ke database, jika validasi lolos. Objek yang dihasilkan dikembalikan apakah objek berhasil disimpan ke database atau tidak."

Anda dapat membuat banyak objek seperti ini:

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

Kemudian, untuk setiap induk, Anda juga dapat menggunakan create untuk ditambahkan ke asosiasinya:

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

Saya merekomendasikan membaca dokumentasi ActiveRecord dan Panduan Rails pada antarmuka kueri ActiveRecord dan asosiasi ActiveRecord . Yang terakhir berisi panduan semua metode yang diperoleh kelas saat Anda mendeklarasikan pengaitan.

Roadmaster
sumber
78
Sayangnya, ActiveRecord akan menghasilkan satu kueri INSERT per model yang dibuat. OP menginginkan panggilan INSERT tunggal, yang tidak akan dilakukan oleh ActiveRecord.
François Beausoleil
Ya, saya berharap mendapatkan semuanya dalam satu panggilan masuk, tetapi jika activerecord tidak begitu pintar, saya kira itu tidak terlalu mudah.
captncraig
@ FrançoisBeausoleil Maukah Anda melihat pertanyaan stackoverflow.com/questions/15386450/… , apakah ini sebabnya saya tidak dapat menyisipkan banyak catatan pada waktu yang sama?
Richlewis
3
Memang benar Anda tidak bisa mendapatkan AR untuk menghasilkan satu INSERT atau UPDATE, tetapi dengan ActiveRecord::Base.transaction { records.each(&:save) }atau serupa Anda setidaknya dapat menempatkan semua INSERT atau UPDATE ke dalam satu transaksi.
yuval
1
Sebenarnya, OP ingin lebih sedikit mengakses database, untuk mempercepat akses DB, dan ActiveRecord sebenarnya memungkinkan Anda melakukannya, dengan mengumpulkan semua panggilan dalam satu transaksi. (Lihat jawaban Harish, yang seharusnya menjadi jawaban yang diterima.) Apa yang ActiveRecord tidak biarkan Anda lakukan adalah membuat DB membuat satu kueri INSERT per transaksi, tetapi itu tidak terlalu penting, karena latensi berasal dari melakukan jaringan akses ke DB, dan tidak di dalam DB itu sendiri ketika melakukan query INSERT.
Magne
99

Karena Anda perlu melakukan banyak penyisipan, database akan dipukul beberapa kali. Penundaan dalam kasus Anda adalah karena setiap penyimpanan dilakukan dalam transaksi DB yang berbeda. Anda dapat mengurangi latensi dengan memasukkan semua operasi Anda dalam satu transaksi.

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

Metode penyimpanan Anda mungkin terlihat seperti ini:

# 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

The savepanggilan pada objek induk menyimpan objek anak-anak.

Harish Shetty
sumber
3
Hanya apa yang saya cari. Mempercepat benih saya. Terima kasih :-)
Renra
16

insert_all (Rel 6+)

Rails 6memperkenalkan metode baru insert_all , yang menyisipkan banyak catatan ke dalam database dalam satu SQL INSERTpernyataan.

Selain itu, metode ini tidak membuat contoh model apa pun dan tidak memanggil callback atau validasi Rekaman Aktif.

Begitu,

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

secara signifikan lebih efisien daripada

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

jika semua yang ingin Anda lakukan adalah memasukkan record baru.

Marian13
sumber
1
Saya tidak bisa menunggu sampai kami memperbarui aplikasi kami. Begitu banyak hal keren di Rails 6.
Dan
1
satu hal yang perlu diperhatikan adalah: insert_all melewatkan callback & validasi AR: edgeguides.rubyonrails.org/…
sujay
11

Salah satu dari dua jawaban ditemukan di tempat lain: oleh Beerlington . Keduanya adalah taruhan terbaik Anda untuk kinerja


Saya pikir kinerja terbaik Anda adalah menggunakan SQL, dan memasukkan banyak baris per kueri secara massal. Jika Anda dapat membuat pernyataan INSERT yang melakukan sesuatu seperti:

SISIPKAN KE foos_bars (foo_id, bar_id) VALUES (1,1), (1,2), (1,3) .... Anda harus dapat memasukkan ribuan baris dalam satu query. Saya tidak mencoba metode mass_habtm Anda, tetapi sepertinya Anda bisa melakukan sesuatu seperti:


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

Juga, jika Anda mencari Bilah dengan "some_attribute", pastikan Anda memiliki bidang yang diindeks di database Anda.


ATAU

Anda mungkin masih melihat activerecord-import. Benar bahwa itu tidak berfungsi tanpa model, tetapi Anda dapat membuat Model hanya untuk diimpor.


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

Bersulang

Nguyen Chien Cong
sumber
Itu berfungsi dengan baik untuk memasukkan, tetapi bagaimana dengan memperbarui beberapa catatan dalam satu transaksi?
Avishai
2
Untuk memperbarui, Anda harus menggunakan upsert: github.com/seamusabshere/upsert . tepuk tangan
Nguyen Chien Cong
Ide yang sangat buruk dengan query sql. Anda harus menggunakan ActiveRecord dan transaksi.
Kerozu
Itu bukan ide yang buruk. Jika Anda melakukan SATU penyisipan, itu akan berhasil atau gagal, saya kira tidak perlu transaksi. Atau Anda selalu dapat membungkus SATU sisipan itu dalam blok transaksi.
Fernando Fabreti
ini adalah praktik rel yang buruk
Blair Anderson
0

Anda perlu menggunakan permata ini "FastInserter" -> https://github.com/joinhandshake/fast_inserter

dan memasukkan sejumlah besar dan ribuan catatan cepat karena permata ini melewatkan catatan aktif, dan hanya menggunakan satu kueri mentah sql

val caro
sumber
1
Meskipun tautan ke permata mungkin berguna, harap berikan beberapa kode yang dapat digunakan Penanya sebagai pengganti kode mereka saat ini (lihat pertanyaan).
trincot
1
Jawaban harus memiliki informasi penting yang tertanam . Harap edit jawaban Anda dan tambahkan tautan di sana, dan tambahkan juga bagian penting dari jawaban itu di dalam jawaban, sehingga dapat berdiri sendiri.
trincot
-2

Anda tidak perlu permata untuk mencapai DB dengan cepat dan hanya sekali!

Jackrg telah mengerjakannya untuk kami: https://gist.github.com/jackrg/76ade1724 E5E5E516292e4e

Fernando Fabreti
sumber
Ada solusi seperti ini untuk Mongodb?
Breno