Come eseguire un aggiornamento raw sql con binding dinamico in rails


98

Voglio eseguire un aggiornamento raw sql come di seguito:

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

Questo SQL verrà eseguito da ActiveRecord::Base.connection.execute, ma non so come passare i valori dei parametri dinamici nel metodo.

Qualcuno potrebbe darmi un aiuto su questo?


Perché vuoi farlo usando SQL grezzo, lo scopo di ActiveRecord è aiutarti a evitare che ...
Andrew

se uso AR, per prima cosa dovrei ottenere Model Object dal metodo find di AR con campo id, quindi eseguire l'operazione di aggiornamento. Quindi dal punto di vista delle operazioni un UPDATE AR necessita di due sqls con database; d'altra parte non sono sicuro che il metodo di aggiornamento di AR utilizzi l'associazione dinamica. quindi voglio utilizzare sql grezzo con associazione dinamica per un'interazione con db per l'operazione di aggiornamento, ma non so come passare i parametri per sostituire il? in sql di AR.
ywenbo

27
Ci sono molte valide ragioni per farlo. Primo, la query potrebbe essere troppo complessa per essere tradotta usando il normale modo Ruby ... secondo, i parametri possono avere caratteri speciali come% o virgolette, ed è un rompicoglioni sfuggire al ..
Henley Chiu

3
@Andrew, è meglio usare le funzioni mysql non elaborate che accettare la "comodità" offerta da AR.
Verde

4
@Green no se vuoi spostare la tua app da MySQL a PostgreSQL o qualcos'altro. Uno dei punti principali di un ORM è rendere la tua app portatile.
Andrew

Risposte:


102

Non sembra che l'API di Rails esponga i metodi per farlo in modo generico. Potresti provare ad accedere alla connessione sottostante e utilizzare i suoi metodi, ad esempio per MySQL:

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

Non sono sicuro che ci siano altre conseguenze nel farlo (connessioni lasciate aperte, ecc.). Traccerei il codice Rails per un normale aggiornamento per vedere cosa sta facendo a parte la query effettiva.

L'uso di query preparate può farti risparmiare una piccola quantità di tempo nel database, ma a meno che tu non lo stia facendo un milione di volte di seguito, probabilmente faresti meglio a creare l'aggiornamento con la normale sostituzione di Ruby, ad es.

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

o utilizzando ActiveRecord come hanno detto i commentatori.


26
Fai attenzione al metodo suggerito di "sostituzione Ruby normale" field=#{value}che ti lascia aperto agli attacchi di SQL injection. Se segui quel percorso, controlla il modulo ActiveRecord :: ConnectionAdapters :: Quoting .
Paul Annesley

3
Nota, mysql2 non supporta le istruzioni preparate, vedi anche: stackoverflow.com/questions/9906437/…
reto

1
@reto Sembra che siano vicini però: github.com/brianmario/mysql2/pull/289
Brian Deterling

3
Nessuna preparedichiarazione in Mysql2
Green

1
Secondo la documentazione: "Nota: a seconda del connettore del database, il risultato restituito da questo metodo potrebbe essere gestito manualmente dalla memoria. Considerare invece l'utilizzo del wrapper #exec_query." . executeè un metodo pericoloso e può causare perdite di memoria o altre risorse.
kolen

32

ActiveRecord::Base.connectionha un quotemetodo che accetta un valore stringa (e facoltativamente l'oggetto colonna). Quindi puoi dire questo:

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

Nota se sei in una migrazione di Rails o in un oggetto ActiveRecord puoi abbreviarlo a:

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

AGGIORNAMENTO: come sottolinea @kolen, dovresti usare exec_updateinvece. Questo gestirà la citazione per te ed eviterà anche perdite di memoria. La firma funziona in modo leggermente diverso però:

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

Qui l'ultimo parametro è un array di tuple che rappresentano i parametri di bind. In ogni tupla, la prima voce è il tipo di colonna e la seconda è il valore. Puoi dare nilper il tipo di colonna e Rails di solito farà la cosa giusta.

Ci sono anche exec_query, exec_inserte exec_delete, a seconda di ciò di cui hai bisogno.


3
Secondo la documentazione: "Nota: a seconda del connettore del database, il risultato restituito da questo metodo potrebbe essere gestito manualmente dalla memoria. Considerare invece l'utilizzo del wrapper #exec_query." . executeè un metodo pericoloso e può causare perdite di memoria o altre risorse.
kolen

1
Wow buona cattura! Ecco la documentazione . Inoltre sembra che l'adattatore Postgres perderebbe qui.
Paul A Jungwirth

Mi chiedo, se AR ci lascia senza nulla on duplicate key updatedappertutto
Shrinath

1
@Shrinath poiché questa domanda riguarda la scrittura di SQL grezzo, puoi fare la query ON CONFLICT DO UPDATEse lo desideri. Per SQL non raw questa gemma sembra utile: github.com/jesjos/active_record_upsert
Paul A Jungwirth

4

Dovresti semplicemente usare qualcosa come:

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

Quello farebbe il trucco. L'uso del metodo ActiveRecord :: Base # send per invocare sanitize_sql_for_assignment fa sì che Ruby (almeno la versione 1.8.7) salti il ​​fatto che sanitize_sql_for_assignment è effettivamente un metodo protetto.


2

A volte sarebbe meglio usare il nome della classe genitore invece del nome della tabella:

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

Ad esempio, classe base "Persona", sottoclassi (e tabelle database) "Cliente" e "Venditore" invece utilizzando:

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

Puoi usare l'oggetto della classe base in questo modo:

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

1

Perché usare SQL grezzo per questo?

Se hai un modello per esso usa 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

Se non hai un modello per esso (solo la tabella), puoi creare un file e un modello che erediteranno daActiveRecord::Base

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

e di nuovo usa wherelo stesso come sopra:


2
Questo è molto meno efficiente, non è vero. Questo non scambia una dichiarazione di aggiornamento per una selezione, porta i record nella memoria di rails, aggiorna ogni record e invia una dichiarazione di aggiornamento per ogni record. 1 aggiornamento contro 1 per record e molta larghezza di banda, ecc.? AR lo ottimizza o no? Non credo lo sia.
Jimi Kimble

0

Ecco un trucco che ho recentemente elaborato per eseguire sql raw con bind:

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

dove lo ApplicationRecorddefinisce:

# 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

e questo è simile a come AR lega le proprie query.


-11

Avevo bisogno di usare sql grezzo perché non sono riuscito a far funzionare composite_primary_keys con activerecord 2.3.8. Quindi, per accedere alla tabella sqlserver 2000 con una chiave primaria composita, era richiesto sql non elaborato.

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

Se è disponibile una soluzione migliore, condividila.


10
sql-injection sono possibili qui!
mystdeim

-18

In Rails 3.1, dovresti usare l'interfaccia di query:

  • nuovo (attributi)
  • create (attributi)
  • create! (attributi)
  • trova (id_or_array)
  • distruggi (id_or_array)
  • distruggere_all
  • elimina (id_or_array)
  • cancella tutto
  • aggiornamento (ID, aggiornamenti)
  • update_all (aggiornamenti)
  • esiste?

update e update_all sono le operazioni di cui hai bisogno.

Vedi i dettagli qui: http://m.onkey.org/active-record-query-interface

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.