Cara menjalankan pembaruan mentah sql dengan pengikatan dinamis di rel


98

Saya ingin menjalankan satu pembaruan raw sql seperti di bawah ini:

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

SQL ini akan dijalankan oleh ActiveRecord::Base.connection.execute, tetapi saya tidak tahu cara meneruskan nilai parameter dinamis ke dalam metode.

Bisakah seseorang memberi saya bantuan?


Mengapa Anda ingin melakukan ini menggunakan SQL mentah, tujuan dari ActiveRecord adalah membantu Anda menghindari itu ...
Andrew

jika saya menggunakan AR, pertama-tama saya harus mendapatkan Model Object dengan metode find AR dengan field id, kemudian melakukan operasi update. Jadi dari tampilan operasi, satu UPDATE AR membutuhkan dua sqls dengan database; di sisi lain saya tidak yakin metode pembaruan AR menggunakan pengikatan dinamis. jadi saya ingin menggunakan sql mentah dengan pengikatan dinamis untuk satu interaksi dengan db untuk operasi pembaruan, tetapi saya tidak tahu cara melewatkan parameter untuk menggantikan? di sql oleh AR.
ywenbo

27
Ada banyak alasan yang sah untuk melakukan ini. Pertama, kueri mungkin terlalu rumit untuk diterjemahkan ke dalam menggunakan cara Ruby biasa ... kedua, parameter mungkin memiliki karakter khusus seperti%, atau tanda kutip, dan sulit untuk melarikan diri dari ..
Henley Chiu

3
@ Andrew, lebih baik menggunakan fungsi mysql mentah daripada menerima "kemudahan" yang ditawarkan AR.
Hijau

4
@Green tidak jika Anda ingin memindahkan aplikasi Anda dari MySQL ke PostgreSQL atau yang lainnya. Salah satu poin utama ORM adalah menjadikan aplikasi Anda portabel.
Andrew

Jawaban:


102

Sepertinya Rails API tidak mengekspos metode untuk melakukan ini secara umum. Anda dapat mencoba mengakses koneksi yang mendasarinya dan menggunakan metodenya, misalnya untuk MySQL:

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

Saya tidak yakin apakah ada konsekuensi lain untuk melakukan ini (koneksi dibiarkan terbuka, dll). Saya akan melacak kode Rails untuk pembaruan normal untuk melihat apa yang dilakukannya selain dari kueri yang sebenarnya.

Menggunakan kueri yang telah disiapkan dapat menghemat sedikit waktu Anda di database, tetapi kecuali Anda melakukan ini jutaan kali berturut-turut, Anda mungkin lebih baik hanya membuat pembaruan dengan substitusi Ruby biasa, misalnya

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

atau menggunakan ActiveRecord seperti yang dikatakan para pemberi komentar.


26
Berhati-hatilah dengan metode "substitusi Ruby normal" yang disarankan field=#{value}karena ini membuat Anda terbuka lebar terhadap serangan injeksi SQL. Jika Anda pergi ke jalur itu, periksa modul ActiveRecord :: ConnectionAdapters :: Quoting .
Paul Annesley

3
Harap dicatat, mysql2 tidak mendukung pernyataan yang disiapkan, lihat juga: stackoverflow.com/questions/9906437/…
reto

1
@reto Sepertinya mereka dekat: github.com/brianmario/mysql2/pull/289
Brian Deterling

3
Tidak ada preparepernyataan di Mysql2
Hijau

1
Menurut dokumentasi: "Catatan: bergantung pada konektor database Anda, hasil yang dikembalikan oleh metode ini mungkin dikelola memori secara manual. Pertimbangkan untuk menggunakan #exec_query wrapper sebagai gantinya." . executeadalah metode berbahaya dan dapat menyebabkan kebocoran memori atau sumber daya lainnya.
kolen

32

ActiveRecord::Base.connectionmemiliki quotemetode yang mengambil nilai string (dan secara opsional objek kolom). Jadi Anda bisa mengatakan ini:

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

Perhatikan jika Anda berada dalam migrasi Rails atau objek ActiveRecord, Anda dapat mempersingkatnya menjadi:

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

PEMBARUAN: Seperti yang ditunjukkan @kolen, Anda harus menggunakan exec_updatesebagai gantinya. Ini akan menangani kutipan untuk Anda dan juga menghindari kebocoran memori. Tanda tangannya bekerja sedikit berbeda:

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

Di sini parameter terakhir adalah larik tupel yang mewakili parameter bind. Di setiap tupel, entri pertama adalah tipe kolom dan yang kedua adalah nilainya. Anda dapat memberikan niluntuk jenis kolom dan Rails biasanya akan melakukan hal yang benar.

Ada juga exec_query,, exec_insertdan exec_delete, tergantung pada apa yang Anda butuhkan.


3
Menurut dokumentasi: "Catatan: bergantung pada konektor database Anda, hasil yang dikembalikan oleh metode ini mungkin dikelola memori secara manual. Pertimbangkan untuk menggunakan #exec_query wrapper sebagai gantinya." . executeadalah metode berbahaya dan dapat menyebabkan kebocoran memori atau sumber daya lainnya.
kolen

1
Wow, tangkapan yang bagus! Berikut dokumentasinya . Juga sepertinya adaptor Postgres akan bocor di sini.
Paul A Jungwirth

Bertanya-tanya, jika AR membuat kita tidak punya apa-apa di on duplicate key updatemana
Shrinath

1
@Shrinath karena pertanyaan ini tentang menulis SQL mentah, Anda dapat membuat kueri ON CONFLICT DO UPDATEjika mau. Untuk SQL non-mentah, permata ini terlihat berguna: github.com/jesjos/active_record_upsert
Paul A Jungwirth

4

Anda sebaiknya menggunakan sesuatu seperti:

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

Itu akan berhasil. Menggunakan metode ActiveRecord :: Base # send untuk memanggil sanitize_sql_for_assignment membuat Ruby (setidaknya versi 1.8.7) mengabaikan fakta bahwa sanitize_sql_for_assignment sebenarnya adalah metode yang dilindungi.


2

Kadang akan lebih baik menggunakan nama kelas induk daripada nama tabel:

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

Misalnya kelas dasar "Orang", subkelas (dan tabel database) "Klien" dan "Penjual" Sebagai gantinya menggunakan:

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

Anda dapat menggunakan objek kelas dasar dengan cara ini:

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

1

Mengapa menggunakan SQL mentah untuk ini?

Jika Anda memiliki model untuk itu gunakan 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

Jika Anda tidak memiliki model untuk itu (hanya tabel), Anda dapat membuat file dan model yang akan diwarisiActiveRecord::Base

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

dan sekali lagi gunakan whereyang sama seperti di atas:


2
Ini jauh lebih tidak efisien, bukan. Tidakkah ini menukar satu pernyataan pembaruan untuk yang dipilih, membawa catatan ke dalam memori rel, memperbarui setiap catatan dan mengirim kembali pernyataan pembaruan untuk setiap catatan. 1 pembaruan vs 1 per catatan dan banyak bandwidth, dll.? Apakah AR mengoptimalkan ini atau tidak? Saya tidak berpikir demikian.
Jimi Kimble

0

Inilah trik yang baru-baru ini saya lakukan untuk mengeksekusi sql mentah dengan binds:

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

dimana ApplicationRecordmendefinisikan ini:

# 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

dan itu mirip dengan cara AR mengikat kuerinya sendiri.


-11

Saya perlu menggunakan sql mentah karena saya gagal mendapatkan composite_primary_keys berfungsi dengan activerecord 2.3.8. Jadi untuk mengakses tabel sqlserver 2000 dengan kunci utama komposit, diperlukan sql mentah.

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

Jika solusi yang lebih baik tersedia, silakan bagikan.


11
sql-injection dimungkinkan di sini!
mystdeim

-19

Di Rails 3.1, Anda harus menggunakan antarmuka kueri:

  • baru (atribut)
  • buat (atribut)
  • buat! (atribut)
  • temukan (id_or_array)
  • hancurkan (id_or_array)
  • destruksi_semua
  • hapus (id_or_array)
  • Hapus semua
  • perbarui (id, pembaruan)
  • update_all (pembaruan)
  • ada?

update dan update_all adalah operasi yang Anda butuhkan.

Lihat detailnya di sini: http://m.onkey.org/active-record-query-interface

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.