บันทึกวัตถุหลายรายการในการโทรครั้งเดียวในราง


93

ฉันมีวิธีในรางที่ทำสิ่งนี้:

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)

ซึ่งจะทำได้ทั้งหมดในฐานข้อมูลสอง Hit เท่านั้น มีวิธีง่ายๆในการทำสิ่งนี้ในรางหรือฉันติดอยู่ที่ทำทีละครั้ง?

คำตอบ:


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 หลังนี้มีคำแนะนำของวิธีการทั้งหมดที่คลาสจะได้รับเมื่อคุณประกาศการเชื่อมโยง


78
ขออภัย ActiveRecord จะสร้างแบบสอบถาม INSERT หนึ่งรายการต่อโมเดลที่สร้างขึ้น OP ต้องการการเรียก INSERT เพียงครั้งเดียวซึ่ง ActiveRecord จะไม่ทำ
François Beausoleil

ใช่ฉันหวังว่าจะได้รับทั้งหมดในการแทรกครั้งเดียว แต่ถ้าแอคทีฟคอร์ดไม่ฉลาดขนาดนั้นฉันเดาว่ามันไม่ง่ายเลย
captncraig

@ FrançoisBeausoleilคุณช่วยดูคำถามstackoverflow.com/questions/15386450/…นี่จะเป็นสาเหตุที่ฉันไม่สามารถแทรกหลายระเบียนในเวลาเดียวกันได้หรือไม่?
Richlewis

3
เป็นเรื่องจริงที่คุณไม่สามารถรับ AR เพื่อสร้าง INSERT หรือ UPDATE ได้ แต่ActiveRecord::Base.transaction { records.each(&:save) }อย่างน้อยก็สามารถใส่ INSERTs หรือUPDATE ทั้งหมดลงในธุรกรรมเดียวได้
yuval

1
ที่จริงแล้ว OP ต้องการตีฐานข้อมูลให้น้อยลงเพื่อเพิ่มความเร็วในการเข้าถึงฐานข้อมูลและ ActiveRecord จะช่วยให้คุณทำเช่นนั้นได้โดยรวมการโทรทั้งหมดในธุรกรรมเดียว (ดูคำตอบของ Harish ซึ่งน่าจะเป็นคำตอบที่ยอมรับได้) สิ่งที่ ActiveRecord ไม่ยอมให้คุณทำคือทำให้ DB สร้างแบบสอบถาม INSERT หนึ่งรายการต่อหนึ่งธุรกรรม แต่นั่นไม่สำคัญมากนักเนื่องจากเวลาแฝงมาจากการทำเครือข่าย เข้าถึงฐานข้อมูลและไม่อยู่ภายในฐานข้อมูลเมื่อทำแบบสอบถาม INSERT
Magne

99

เนื่องจากคุณต้องทำการแทรกหลาย ๆ ฐานข้อมูลจึงจะถูกโจมตีหลายครั้ง ความล่าช้าในกรณีของคุณเป็นเพราะการบันทึกแต่ละครั้งทำในธุรกรรม DB ที่แตกต่างกัน คุณสามารถลดเวลาแฝงได้โดยการรวมการดำเนินการทั้งหมดของคุณไว้ในธุรกรรมเดียว

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

16

insert_all (ราง 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
Dan

2
สิ่งหนึ่งที่ควรทราบคือ: insert_all ข้ามการเรียกกลับและการตรวจสอบความถูกต้องของ AR: edgeguides.rubyonrails.org/…
sujay

11

หนึ่งในสองคำตอบที่พบที่อื่น: โดยBeerlington ทั้งสองเป็นทางออกที่ดีที่สุดสำหรับประสิทธิภาพ


ฉันคิดว่าประสิทธิภาพที่ดีที่สุดของคุณคือการใช้ SQL และแทรกหลายแถวต่อแบบสอบถาม หากคุณสามารถสร้างคำสั่ง INSERT ที่ทำสิ่งที่ต้องการ:

INSERT INTO foos_bars (foo_id, bar_id) VALUES (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}")

นอกจากนี้หากคุณกำลังค้นหา Bar โดย "some_attribute" ตรวจสอบให้แน่ใจว่าคุณได้จัดทำดัชนีฟิลด์นั้นไว้ในฐานข้อมูลของคุณแล้ว


หรือ

คุณยังอาจได้ดู activerecord-import เป็นเรื่องถูกต้องที่จะใช้งานไม่ได้หากไม่มีโมเดล แต่คุณสามารถสร้างโมเดลสำหรับการนำเข้าเท่านั้น


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

ไชโย


มันใช้งานได้ดีสำหรับการแทรก แต่ถ้าจะอัปเดตหลายระเบียนในธุรกรรมเดียวล่ะ
Avishai

2
สำหรับการปรับปรุงที่คุณควรใช้ upsert: github.com/seamusabshere/upsert ไชโย
เหงียนเชียนกง

ความคิดที่แย่มากกับแบบสอบถาม sql คุณควรใช้ ActiveRecord และธุรกรรม
Kerozu

ไม่ใช่ความคิดที่เลวร้าย หากคุณกำลังทำการแทรกเพียงครั้งเดียวมันจะสำเร็จหรือล้มเหลวฉันเดาว่าไม่จำเป็นต้องทำธุรกรรม หรือคุณสามารถห่อหนึ่งแทรกในบล็อกธุรกรรมได้ตลอดเวลา
Fernando Fabreti

นี่คือการฝึกฝนที่ไม่ดี
Blair Anderson

0

คุณต้องใช้อัญมณีนี้ "FastInserter" -> https://github.com/joinhandshake/fast_inserter

และการแทรกจำนวนมากและหลายพันระเบียนทำได้อย่างรวดเร็วเนื่องจากอัญมณีนี้ข้ามระเบียนที่ใช้งานอยู่และใช้แบบสอบถามดิบ sql เพียงรายการเดียว


1
แม้ว่าลิงก์ไปยังอัญมณีอาจมีประโยชน์โปรดระบุรหัสบางอย่างที่ผู้ถามสามารถใช้แทนรหัสปัจจุบันของพวกเขาได้ (ดูคำถาม)
trincot


1
คำตอบจะต้องมีข้อมูลที่จำเป็นที่ฝังตัว โปรดแก้ไขคำตอบของคุณและเพิ่มลิงก์ที่นั่นและเพิ่มส่วนสำคัญของคำตอบนั้นในคำตอบเพื่อให้เป็นคำตอบในตัวเอง
trincot

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.