Rails สร้างหรืออัพเดตเวทย์มนตร์?


95

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

มีวิธีทำใน Rails หรือไม่หรือต้องเขียน method ของตัวเอง?

คำตอบ:


175

ราง 6

Rails 6 ได้เพิ่มupsertและupsert_allวิธีการที่ให้ฟังก์ชันนี้

Model.upsert(column_name: value)

[upsert] มันไม่ได้สร้างอินสแตนซ์โมเดลใด ๆ และไม่ทริกเกอร์การเรียกกลับหรือการตรวจสอบ Active Record

ราง 5, 4 และ 3

ไม่ใช่ถ้าคุณกำลังมองหาคำสั่งประเภท "upsert" (ที่ฐานข้อมูลเรียกใช้การอัปเดตหรือคำสั่งแทรกในการดำเนินการเดียวกัน) Rails และ ActiveRecord นอกกรอบไม่มีคุณสมบัติดังกล่าว อย่างไรก็ตามคุณสามารถใช้อัญมณีเสริมดวงได้

มิฉะนั้นคุณสามารถใช้: find_or_initialize_byหรือfind_or_create_byซึ่งมีฟังก์ชันการทำงานที่คล้ายคลึงกันแม้ว่าจะมีค่าใช้จ่ายในการตีฐานข้อมูลเพิ่มเติมซึ่งในกรณีส่วนใหญ่แทบจะไม่เป็นปัญหาเลย ดังนั้นหากคุณไม่กังวลเรื่องประสิทธิภาพอย่างจริงจังฉันจะไม่ใช้อัญมณี

ตัวอย่างเช่นหากไม่พบผู้ใช้ที่มีชื่อ "Roger" อินสแตนซ์ผู้ใช้ใหม่จะถูกสร้างอินสแตนซ์โดยnameตั้งค่าเป็น "Roger"

user = User.where(name: "Roger").first_or_initialize
user.email = "email@example.com"
user.save

find_or_initialize_byหรือคุณสามารถใช้

user = User.find_or_initialize_by(name: "Roger")

ใน Rails 3.

user = User.find_or_initialize_by_name("Roger")
user.email = "email@example.com"
user.save

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

User.where(name: "Roger").first_or_initialize do |user|
  # this won't run if a user with name "Roger" is found
  user.save 
end

User.find_or_initialize_by(name: "Roger") do |user|
  # this also won't run if a user with name "Roger" is found
  user.save
end

หากคุณต้องการใช้บล็อกโดยไม่คำนึงถึงความคงอยู่ของเรกคอร์ดให้ใช้tapกับผลลัพธ์:

User.where(name: "Roger").first_or_initialize.tap do |user|
  user.email = "email@example.com"
  user.save
end


1
ซอร์สโค้ดที่เอกสารชี้ให้เห็นว่าสิ่งนี้ไม่ได้ผลตามที่คุณบอก - บล็อกจะถูกส่งต่อไปยังวิธีการใหม่ก็ต่อเมื่อไม่มีเรกคอร์ดที่เกี่ยวข้อง ดูเหมือนว่าจะไม่มีเวทย์มนตร์ "เพิ่มขึ้น" ใน Rails - คุณต้องแยกมันออกเป็นสองประโยค Ruby หนึ่งสำหรับการเลือกอ็อบเจกต์และอีกอันสำหรับการอัพเดตแอ็ตทริบิวต์
เหมือน

@sameers ฉันไม่แน่ใจว่าฉันเข้าใจสิ่งที่คุณหมายถึง คุณคิดว่าฉันหมายถึงอะไร?
Mohamad

1
โอ้ ... ฉันเข้าใจว่าตอนนี้คุณหมายถึงอะไร - ทั้งสองรูปแบบfind_or_initialize_byและfind_or_create_byยอมรับการบล็อก ฉันคิดว่าคุณหมายความว่าไม่ว่าจะมีบันทึกอยู่หรือไม่บล็อกจะถูกส่งต่อไปพร้อมกับวัตถุบันทึกเป็นอาร์กิวเมนต์เพื่อทำการอัปเดต
เหมือน

3
มันทำให้เข้าใจผิดเล็กน้อยไม่จำเป็นต้องเป็นคำตอบ แต่เป็น API เราคาดหวังว่าบล็อกจะถูกส่งผ่านโดยไม่คำนึงถึงดังนั้นจึงสามารถสร้าง / อัปเดตได้ตามนั้น แต่เราต้องแยกมันออกเป็นงบแยกต่างหาก บู. <3 Rails แม้ว่า :)
Volte

32

ใน Rails 4 คุณสามารถเพิ่มลงในโมเดลเฉพาะ:

def self.update_or_create(attributes)
  assign_or_new(attributes).save
end

def self.assign_or_new(attributes)
  obj = first || new
  obj.assign_attributes(attributes)
  obj
end

และใช้มันเช่น

User.where(email: "a@b.com").update_or_create(name: "Mr A Bbb")

หรือหากคุณต้องการเพิ่มวิธีการเหล่านี้ให้กับทุกรุ่นที่ใส่ใน initializer:

module ActiveRecordExtras
  module Relation
    extend ActiveSupport::Concern

    module ClassMethods
      def update_or_create(attributes)
        assign_or_new(attributes).save
      end

      def update_or_create!(attributes)
        assign_or_new(attributes).save!
      end

      def assign_or_new(attributes)
        obj = first || new
        obj.assign_attributes(attributes)
        obj
      end
    end
  end
end

ActiveRecord::Base.send :include, ActiveRecordExtras::Relation

1
จะไม่assign_or_newส่งคืนแถวแรกในตารางหากมีอยู่แล้วแถวนั้นจะได้รับการอัปเดต? ดูเหมือนว่าจะทำอย่างนั้นสำหรับฉัน
steve klein

User.where(email: "a@b.com").firstจะคืนค่าศูนย์หากไม่พบ ตรวจสอบให้แน่ใจว่าคุณมีwhereขอบเขต
montrealmike

เพียงบันทึกไว้ว่าupdated_atจะไม่แตะต้องเพราะassign_attributesใช้แล้ว
lshepstone

คุณจะไม่ใช้ assing_or_new แต่ถ้าคุณใช้ update_or_create เนื่องจากการบันทึก
montrealmike

13

เพิ่มสิ่งนี้ลงในโมเดลของคุณ:

def self.update_or_create_by(args, attributes)
  obj = self.find_or_create_by(args)
  obj.update(attributes)
  return obj
end

ด้วยวิธีนี้คุณสามารถ:

User.update_or_create_by({name: 'Joe'}, attributes)

ส่วนที่สองฉันจะไม่ทำงาน ไม่สามารถอัปเดตระเบียนเดียวจากระดับชั้นเรียนโดยไม่มี ID ของระเบียนที่จะอัปเดต
Aeramor

2
obj = self.find_or_create_by (args); obj.update (แอตทริบิวต์); ส่งคืน obj จะทำงาน
veeresh yh

12

เวทมนตร์ที่คุณกำลังมองหาได้ถูกเพิ่มเข้าไปในRails 6 ตอนนี้คุณสามารถอัพอัพได้ (อัปเดตหรือแทรก) สำหรับการใช้งานแบบบันทึกครั้งเดียว:

Model.upsert(column_name: value)

สำหรับหลายระเบียนให้ใช้upsert_all :

Model.upsert_all(column_name: value, unique_by: :column_name)

หมายเหตุ :

  • ทั้งสองวิธีไม่ทริกเกอร์การเรียกกลับหรือการตรวจสอบ Active Record
  • unique_by => PostgreSQL และ SQLite เท่านั้น

1

คำถามเก่า แต่โยนคำตอบของฉันลงในวงแหวนเพื่อความสมบูรณ์ ฉันต้องการสิ่งนี้เมื่อฉันต้องการการค้นหาที่เฉพาะเจาะจง แต่การสร้างอื่นหากไม่มีอยู่

def self.find_by_or_create_with(args, attributes) # READ CAREFULLY! args for finding, attributes for creating!
        obj = self.find_or_initialize_by(args)
        return obj if obj.persisted?
        return obj if obj.update_attributes(attributes) 
end

0

คุณสามารถทำได้ในคำสั่งเดียวดังนี้:

CachedObject.where(key: "the given key").first_or_create! do |cached|
   cached.attribute1 = 'attribute value'
   cached.attribute2 = 'attribute value'
end

9
การดำเนินการนี้จะใช้ไม่ได้เนื่องจากจะส่งคืนระเบียนเดิมหากพบเท่านั้น OP จะขอวิธีแก้ปัญหาที่เปลี่ยนแปลงค่าเสมอแม้ว่าจะพบระเบียนก็ตาม
JeanMertz

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