Cómo ejecutar un sql de actualización sin procesar con enlace dinámico en rieles

98

Quiero ejecutar una actualización de SQL sin formato como se muestra a continuación:

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

Este SQL será ejecutado por ActiveRecord::Base.connection.execute, pero no sé cómo pasar los valores de los parámetros dinámicos al método.

¿Alguien podría ayudarme?

ywenbo
fuente
¿Por qué quiere hacer esto usando SQL sin formato? El objetivo de ActiveRecord es ayudarlo a evitar eso ...
Andrew
Si uso AR, en primer lugar debería obtener el objeto modelo mediante el método de búsqueda de AR con el campo de identificación, luego realizar la operación de actualización. Entonces, desde el punto de vista de las operaciones, una UPDATE AR necesita dos sqls con base de datos; por otro lado, no estoy seguro de que el método de actualización de AR utilice enlace dinámico. así que quiero usar sql sin procesar con enlace dinámico para una interacción con db para la operación de actualización, pero no sé cómo pasar parámetros para reemplazar el? en sql por AR.
ywenbo
27
Hay muchas razones válidas para hacer esto. Primero, la consulta puede ser demasiado compleja para traducirla en el uso regular de Ruby ... segundo, los parámetros pueden tener caracteres especiales como% o comillas, y es un dolor de cabeza escapar del ...
Henley Chiu
3
@ Andrew, es mejor usar funciones de mysql sin procesar que aceptar la "conveniencia" que ofrece AR.
Verde
4
@Green, no si alguna vez desea mover su aplicación de MySQL a PostgreSQL o algo más. Uno de los puntos principales de un ORM es hacer que su aplicación sea portátil.
Andrew

Respuestas:

102

No parece que la API de Rails exponga métodos para hacer esto de forma genérica. Puede intentar acceder a la conexión subyacente y usar sus métodos, por ejemplo, para MySQL:

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

No estoy seguro de si hay otras ramificaciones para hacer esto (conexiones abiertas, etc.). Seguiría el código de Rails para una actualización normal para ver qué está haciendo aparte de la consulta real.

El uso de consultas preparadas puede ahorrarle una pequeña cantidad de tiempo en la base de datos, pero a menos que esté haciendo esto un millón de veces seguidas, probablemente sea mejor que construya la actualización con la sustitución normal de Ruby, por ejemplo

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

o usando ActiveRecord como dijeron los comentaristas.

Brian Deterling
fuente
26
Tenga cuidado con el método sugerido de "sustitución normal de Ruby", field=#{value}ya que esto lo deja abierto a ataques de inyección SQL. Si sigue ese camino, consulte el módulo ActiveRecord :: ConnectionAdapters :: Quoting .
Paul Annesley
3
Tenga en cuenta que mysql2 no admite declaraciones preparadas, consulte también: stackoverflow.com/questions/9906437/…
reto
1
@reto Parece que están cerca: github.com/brianmario/mysql2/pull/289
Brian Deterling
3
Sin preparedeclaración en Mysql2
Verde
1
Según la documentación: "Nota: según el conector de su base de datos, el resultado devuelto por este método puede administrarse manualmente en la memoria. Considere usar el contenedor #exec_query en su lugar". . executees un método peligroso y puede provocar pérdidas de memoria u otros recursos.
kolen
32

ActiveRecord::Base.connectiontiene un quotemétodo que toma un valor de cadena (y opcionalmente el objeto de columna). Entonces puedes decir esto:

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

Tenga en cuenta que si está en una migración de Rails o en un objeto ActiveRecord, puede acortarlo a:

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

ACTUALIZACIÓN: como señala @kolen, debería usar exec_updateen su lugar. Esto manejará las cotizaciones por usted y también evitará pérdidas de memoria. Sin embargo, la firma funciona de manera un poco diferente:

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

Aquí, el último parámetro es una matriz de tuplas que representan parámetros de enlace. En cada tupla, la primera entrada es el tipo de columna y la segunda es el valor. Puede ceder nilpor el tipo de columna y Rails normalmente hará lo correcto.

También hay exec_query, exec_inserty exec_delete, dependiendo de lo que necesita.

Paul A Jungwirth
fuente
3
Según la documentación: "Nota: según el conector de su base de datos, el resultado devuelto por este método puede administrarse manualmente en la memoria. Considere usar el contenedor #exec_query en su lugar". . executees un método peligroso y puede provocar pérdidas de memoria u otros recursos.
kolen
1
¡Guau, buena captura! Aquí está la documentación . También parece que el adaptador de Postgres se filtraría aquí.
Paul A Jungwirth
Me pregunto si la realidad aumentada nos deja sin nada para on duplicate key updatetodas partes
Shrinath
1
@Shrinath, dado que esta pregunta se trata de escribir SQL sin formato, puede realizar la consulta ON CONFLICT DO UPDATEsi lo desea. Para SQL no crudo, esta gema parece útil: github.com/jesjos/active_record_upsert
Paul A Jungwirth
4

Deberías usar algo como:

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

Eso serviría. El uso del método de envío ActiveRecord :: Base # para invocar sanitize_sql_for_assignment hace que Ruby (al menos la versión 1.8.7) omita el hecho de que sanitize_sql_for_assignment es en realidad un método protegido.

leandroico
fuente
2

En algún momento sería mejor usar el nombre de la clase principal en lugar del nombre de la tabla:

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

Por ejemplo, clase base "Persona", subclases (y tablas de base de datos) "Cliente" y "Vendedor" En lugar de usar:

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

Puede usar el objeto de la clase base de esta manera:

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

¿Por qué utilizar SQL sin formato para esto?

Si tiene un modelo para usar 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

Si no tiene un modelo para él (solo la tabla), puede crear un archivo y un modelo que heredará deActiveRecord::Base

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

y nuevamente use wherelo mismo que el anterior:

ToTenMilan
fuente
2
Esto es mucho menos eficiente, ¿no? ¿No cambia esto una declaración de actualización por una selección, trae los registros a la memoria de rieles, actualiza cada registro y envía una declaración de actualización para cada registro? 1 actualización vs 1 por registro y mucho ancho de banda, etc. ¿RA optimiza esto o no? No creo que lo haga.
Jimi Kimble
0

Aquí hay un truco que resolví recientemente para ejecutar sql sin procesar con enlaces:

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

donde ApplicationRecorddefine esto:

# 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

y eso es similar a cómo AR vincula sus propias consultas.

espuma
fuente
-11

Necesitaba usar sql sin procesar porque no logré que composite_primary_keys funcionara con activerecord 2.3.8. Entonces, para acceder a la tabla sqlserver 2000 con una clave primaria compuesta, se requería sql sin procesar.

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

Si hay una mejor solución disponible, compártala.

Donvnielsen
fuente
10
¡La inyección sql es posible aquí!
mystdeim
-18

En Rails 3.1, debes usar la interfaz de consulta:

  • nuevo (atributos)
  • crear (atributos)
  • crear! (atributos)
  • buscar (id_or_array)
  • destruir (id_or_array)
  • destruye todo
  • eliminar (id_or_array)
  • eliminar todos
  • actualización (ids, actualizaciones)
  • update_all (actualizaciones)
  • existe?

update y update_all son la operación que necesita.

Vea los detalles aquí: http://m.onkey.org/active-record-query-interface

activars
fuente