วิธีเรียกใช้การอัปเดตแบบดิบ sql ด้วยการผูกแบบไดนามิกในราง


102

ฉันต้องการเรียกใช้การอัปเดตหนึ่งไฟล์ sql ด้านล่าง:

update table set f1=? where f2=? and f3=?

SQL นี้จะถูกเรียกใช้ActiveRecord::Base.connection.executeงาน แต่ฉันไม่รู้ว่าจะส่งค่าพารามิเตอร์ไดนามิกไปยังเมธอดอย่างไร

มีใครให้ความช่วยเหลือฉันได้ไหม


ทำไมคุณถึงต้องการทำสิ่งนี้โดยใช้ SQL ดิบประเด็นของ ActiveRecord คือช่วยให้คุณหลีกเลี่ยงสิ่งนั้น ...
Andrew

ถ้าฉันใช้ AR อันดับแรกฉันควรได้รับ Model Object ด้วยวิธีการค้นหาของ AR พร้อมฟิลด์ id จากนั้นทำการอัปเดต ดังนั้นจากมุมมองของการดำเนินการหนึ่ง UPDATE AR จำเป็นต้องมีสอง sqls กับฐานข้อมูล; ในทางกลับกันฉันไม่แน่ใจว่าวิธีการอัปเดตของ AR ใช้การเชื่อมโยงแบบไดนามิก ดังนั้นฉันต้องการใช้ Raw sql ที่มีการเชื่อมโยงแบบไดนามิกสำหรับการโต้ตอบเดียวกับ db สำหรับการดำเนินการอัปเดต แต่ฉันไม่รู้วิธีส่งผ่านพารามิเตอร์เพื่อแทนที่? ใน sql โดย AR
ywenbo

27
มีหลายเหตุผลที่ถูกต้องในการทำเช่นนี้ ประการแรกแบบสอบถามอาจซับซ้อนเกินไปที่จะแปลโดยใช้วิธีปกติของ Ruby ... ประการที่สองพารามิเตอร์อาจมีอักขระพิเศษเช่น% หรือเครื่องหมายคำพูดและเป็นเรื่องที่เจ็บปวดเมื่อต้องหลบหนี ..
Henley Chiu

3
@ แอนดรูว์ควรใช้ฟังก์ชัน mysql ดิบดีกว่ายอมรับ "ความสะดวก" ที่ AR เสนอ
กรีน

4
@ กรีนไม่ได้หากคุณต้องการย้ายแอพของคุณจาก MySQL ไปยัง PostgreSQL หรืออย่างอื่น จุดสำคัญอย่างหนึ่งของ ORM คือการทำให้แอปพกพาได้
Andrew

คำตอบ:


103

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

st = ActiveRecord::Base.connection.raw_connection.prepare("update table set f1=? where f2=? and f3=?")
st.execute(f1, f2, f3)
st.close

ฉันไม่แน่ใจว่ามีการแบ่งส่วนอื่น ๆ ให้ทำเช่นนี้หรือไม่ (การเชื่อมต่อเปิดทิ้งไว้ ฯลฯ ) ฉันจะติดตามรหัส Rails สำหรับการอัปเดตปกติเพื่อดูว่ามันทำอะไรนอกเหนือจากแบบสอบถามจริง

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

ActiveRecord::Base.connection.execute("update table set f1=#{ActiveRecord::Base.sanitize(f1)}")

หรือใช้ ActiveRecord เหมือนที่ผู้แสดงความคิดเห็นกล่าว


26
ระวังวิธีการ "การแทนที่ Ruby ปกติ" ที่แนะนำfield=#{value}เนื่องจากจะทำให้คุณเปิดกว้างต่อการโจมตีด้วยการฉีด SQL หากคุณไปตามเส้นทางนั้นให้ตรวจสอบโมดูลActiveRecord :: ConnectionAdapters :: Quoting
Paul Annesley

4
โปรดทราบว่า mysql2 ไม่รองรับงบที่เตรียมไว้โปรดดูที่: stackoverflow.com/questions/9906437/…
reto

1
@reto ดูเหมือนว่าพวกเขาจะอยู่ใกล้กัน: github.com/brianmario/mysql2/pull/289
Brian Deterling

3
ไม่มีprepareข้อความใน Mysql2
สีเขียว

1
ตามเอกสาร: "หมายเหตุ: ขึ้นอยู่กับตัวเชื่อมต่อฐานข้อมูลของคุณผลลัพธ์ที่ส่งคืนโดยวิธีนี้อาจได้รับการจัดการหน่วยความจำด้วยตนเองลองใช้ #exec_query wrapper แทน" . executeเป็นวิธีที่อันตรายและอาจทำให้หน่วยความจำหรือทรัพยากรอื่น ๆ รั่วไหล
kolen

33

ActiveRecord::Base.connectionมีquoteวิธีการที่รับค่าสตริง (และอ็อบเจ็กต์คอลัมน์เป็นทางเลือก) ดังนั้นคุณสามารถพูดสิ่งนี้:

ActiveRecord::Base.connection.execute(<<-EOQ)
  UPDATE  foo
  SET     bar = #{ActiveRecord::Base.connection.quote(baz)}
EOQ

สังเกตว่าคุณอยู่ในการโอนย้าย Rails หรือวัตถุ ActiveRecord คุณสามารถย่อให้สั้นลงเป็น:

connection.execute(<<-EOQ)
  UPDATE  foo
  SET     bar = #{connection.quote(baz)}
EOQ

UPDATE:ตามที่ @kolen ชี้ให้เห็นคุณควรใช้exec_updateแทน สิ่งนี้จะจัดการการเสนอราคาให้คุณและหลีกเลี่ยงการรั่วไหลของหน่วยความจำ ลายเซ็นทำงานแตกต่างกันเล็กน้อยแม้ว่า:

connection.exec_update(<<-EOQ, "SQL", [[nil, baz]])
  UPDATE  foo
  SET     bar = $1
EOQ

ที่นี่พารามิเตอร์สุดท้ายคืออาร์เรย์ของสิ่งที่เป็นตัวแทนของพารามิเตอร์การผูก ในแต่ละทูเปิลรายการแรกคือประเภทคอลัมน์และรายการที่สองคือค่า คุณสามารถnilระบุประเภทคอลัมน์และ Rails มักจะทำในสิ่งที่ถูกต้อง

นอกจากนี้ยังมีexec_query, exec_insertและexec_deleteขึ้นอยู่กับสิ่งที่คุณต้องการ


3
ตามเอกสาร: "หมายเหตุ: ขึ้นอยู่กับตัวเชื่อมต่อฐานข้อมูลของคุณผลลัพธ์ที่ส่งคืนโดยวิธีนี้อาจได้รับการจัดการหน่วยความจำด้วยตนเองลองใช้ #exec_query wrapper แทน" . executeเป็นวิธีที่อันตรายและอาจทำให้หน่วยความจำหรือทรัพยากรอื่น ๆ รั่วไหล
kolen

1
ว้าวจับดี! นี่คือเอกสาร ดูเหมือนว่าอะแดปเตอร์ Postgresจะรั่วที่นี่
Paul A Jungwirth

น่าแปลกใจถ้า AR ทำให้เรามีอะไรให้on duplicate key updateทุกที่
Shrinath

1
@Shrinath เนื่องจากคำถามนี้เกี่ยวกับการเขียน SQL ดิบคุณสามารถสร้างแบบสอบถามได้ON CONFLICT DO UPDATEหากต้องการ สำหรับ SQL ที่ไม่ใช่ดิบอัญมณีนี้ดูมีประโยชน์: github.com/jesjos/active_record_upsert
Paul A Jungwirth

4

คุณควรใช้สิ่งต่างๆเช่น:

YourModel.update_all(
  ActiveRecord::Base.send(:sanitize_sql_for_assignment, {:value => "'wow'"})
)

ที่จะทำเคล็ดลับ การใช้เมธอดActiveRecord :: Base # sendเพื่อเรียกใช้sanitize_sql_for_assignmentทำให้ Ruby (อย่างน้อยรุ่น 1.8.7) ข้ามข้อเท็จจริงที่ว่าsanitize_sql_for_assignmentเป็นวิธีการป้องกัน


2

ในบางครั้งจะเป็นการดีกว่าที่จะใช้ชื่อของคลาสหลักแทนชื่อตาราง:

# Refers to the current class
self.class.unscoped.where(self.class.primary_key => id).update_all(created _at: timestamp)

ตัวอย่างเช่นคลาสพื้นฐาน "บุคคล" คลาสย่อย (และตารางฐานข้อมูล) "ไคลเอนต์" และ "ผู้ขาย" แทนที่จะใช้:

Client.where(self.class.primary_key => id).update_all(created _at: timestamp)
Seller.where(self.class.primary_key => id).update_all(created _at: timestamp)

คุณสามารถใช้ออบเจ็กต์ของคลาสพื้นฐานได้ด้วยวิธีนี้:

person.class.unscoped.where(self.class.primary_key => id).update_all(created _at: timestamp)

1

เหตุใดจึงใช้ Raw SQL สำหรับสิ่งนี้

หากคุณมีแบบจำลองให้ใช้where:

f1 = 'foo'
f2 = 'bar'
f3 = 'buzz'
YourModel.where('f1 = ? and f2 = ?', f1, f2).each do |ym|
  # or where(f1: f1, f2: f2).each do (...)
  ym.update(f3: f3) 
end

หากคุณไม่มีโมเดล (เฉพาะตาราง) คุณสามารถสร้างไฟล์และโมเดลที่จะสืบทอดจากActiveRecord::Base

class YourTable < ActiveRecord::Base
  self.table_name = 'your_table' # specify explicitly if needed
end

และอีกครั้งใช้whereเช่นเดียวกับด้านบน:


2
มันมีประสิทธิภาพน้อยกว่ามากใช่ไหม สิ่งนี้ไม่แลกเปลี่ยนคำสั่งการอัปเดตหนึ่งรายการสำหรับการเลือกนำบันทึกลงในหน่วยความจำรางอัปเดตแต่ละระเบียนและส่งคำสั่งอัปเดตกลับสำหรับแต่ละระเบียน การอัปเดต 1 รายการเทียบกับ 1 รายการต่อการบันทึกและแบนด์วิธมาก ฯลฯ ? AR ปรับให้เหมาะสมหรือไม่? ฉันไม่คิดว่ามันจะ
Jimi Kimble

0

นี่คือเคล็ดลับที่ฉันเพิ่งใช้ในการเรียกใช้ไฟล์ raw sql ด้วยการผูก:

binds = SomeRecord.bind(a_string_field: value1, a_date_field: value2) +
        SomeOtherRecord.bind(a_numeric_field: value3)
SomeRecord.connection.exec_query <<~SQL, nil, binds
  SELECT *
  FROM some_records
  JOIN some_other_records ON some_other_records.record_id = some_records.id
  WHERE some_records.a_string_field = $1
    AND some_records.a_date_field < $2
    AND some_other_records.a_numeric_field > $3
SQL

ที่ApplicationRecordกำหนดสิ่งนี้:

# Convenient way of building custom sql binds
def self.bind(column_values)
  column_values.map do |column_name, value|
    [column_for_attribute(column_name), value]
  end
end

และคล้ายกับวิธีที่ AR เชื่อมโยงการสืบค้นของตัวเอง


-11

ฉันจำเป็นต้องใช้ raw sql เพราะฉันล้มเหลวในการรับ composite_primary_keys ให้ทำงานกับ activerecord 2.3.8 ดังนั้นในการเข้าถึงตาราง sqlserver 2000 ด้วยคีย์หลักแบบผสมจำเป็นต้องมี Raw sql

sql = "update [db].[dbo].[#{Contacts.table_name}] " +
      "set [COLUMN] = 0 " +
      "where [CLIENT_ID] = '#{contact.CLIENT_ID}' and CONTACT_ID = '#{contact.CONTACT_ID}'"
st = ActiveRecord::Base.connection.raw_connection.prepare(sql)
st.execute

หากมีวิธีแก้ไขที่ดีกว่าโปรดแบ่งปัน


11
sql-injection ได้ที่นี่!
mystdeim

-21

ใน Rails 3.1 คุณควรใช้อินเทอร์เฟซแบบสอบถาม:

  • ใหม่ (แอตทริบิวต์)
  • สร้าง (แอตทริบิวต์)
  • สร้าง! (แอตทริบิวต์)
  • ค้นหา (id_or_array)
  • ทำลาย (id_or_array)
  • destroy_all
  • ลบ (id_or_array)
  • ลบทั้งหมด
  • อัปเดต (รหัสอัปเดต)
  • update_all (อัปเดต)
  • มีอยู่จริง?

update และ update_all คือการดำเนินการที่คุณต้องการ

ดูรายละเอียดที่นี่: http://m.onkey.org/active-record-query-interface

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