111 votes

Comment exécuter un sql de mise à jour brut avec liaison dynamique dans rails

Je veux exécuter une mise à jour raw sql comme ci-dessous :

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

Ce SQL sera exécuté par ActiveRecord::Base.connection.execute mais je ne sais pas comment transmettre les valeurs des paramètres dynamiques dans la méthode.

Quelqu'un peut-il m'aider ?

0 votes

Pourquoi voulez-vous faire cela en utilisant le SQL brut, le but d'ActiveRecord est de vous aider à éviter cela...

0 votes

Si j'utilise AR, je dois d'abord obtenir Model Object par la méthode find d'AR avec le champ id, puis faire l'opération de mise à jour. Donc, du point de vue des opérations, un UPDATE AR nécessite deux sql avec la base de données ; d'autre part, je ne suis pas sûr que la méthode de mise à jour de AR utilise une liaison dynamique. Je veux donc utiliser un sql brut avec une liaison dynamique pour une interaction avec la base de données pour l'opération de mise à jour, mais je ne sais pas comment passer des paramètres pour remplacer le ? dans le sql par AR.

32 votes

Il y a de nombreuses raisons valables de le faire. Tout d'abord, la requête peut être trop complexe pour être traduite en utilisant la méthode Ruby habituelle... Ensuite, les paramètres peuvent contenir des caractères spéciaux comme % ou des guillemets, et c'est un casse-tête d'échapper les...

113voto

Brian Deterling Points 7778

Il ne semble pas que l'API Rails expose des méthodes pour faire cela de manière générique. Vous pouvez essayer d'accéder à la connexion sous-jacente et utiliser ses méthodes, par exemple pour MySQL :

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

Je ne sais pas s'il y a d'autres conséquences à faire cela (connexions laissées ouvertes, etc.). Je tracerais le code Rails pour une mise à jour normale afin de voir ce qu'il fait en dehors de la requête réelle.

L'utilisation de requêtes préparées peut vous faire gagner un peu de temps dans la base de données, mais à moins que vous ne fassiez cela un million de fois d'affilée, vous feriez probablement mieux de construire la mise à jour avec une substitution Ruby normale, par ex.

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

ou en utilisant ActiveRecord comme l'ont dit les commentateurs.

0 votes

:-), j'apprécie beaucoup votre aide, vous avez raison, si vous avez des mises à jour après avoir tracé le code de rails, faites-le moi savoir. Si vraiment il n'y a pas d'exposition, la méthode que vous proposez sera la meilleure dans mon cas. Je crains que la méthode de mise à jour d'ActiveRecord n'utilise pas de liaison dynamique, si c'est le cas, c'est mauvais.

0 votes

Il semble que vous deviez appeler gratuitement les résultats que vous recevez, mais je ne pense pas que cela s'applique à une mise à jour. Tant que vous fermez la déclaration comme je l'ai montré, tout devrait bien se passer.

0 votes

J'apprécie vraiment votre réponse pour mes problèmes, merci, jusqu'à maintenant c'est clair avec votre aide.

38voto

Paul A Jungwirth Points 3580

ActiveRecord::Base.connection a un quote qui prend une valeur de type chaîne de caractères (et éventuellement l'objet colonne). Vous pouvez donc dire ceci

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

Notez que si vous êtes dans une migration Rails ou un objet ActiveRecord, vous pouvez raccourcir cela en :

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

UPDATE : Comme le souligne @kolen, vous devriez utiliser exec_update à la place. Cela permet de gérer la citation pour vous et d'éviter les fuites de mémoire. La signature fonctionne cependant un peu différemment :

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

Ici, le dernier paramètre est un tableau de tuples représentant les paramètres de liaison. Dans chaque tuple, la première entrée est le type de colonne et la seconde est la valeur. Vous pouvez donner nil pour le type de colonne et Rails fera généralement ce qu'il faut.

Il existe également exec_query , exec_insert y exec_delete en fonction de vos besoins.

3 votes

Selon la documentation : "Remarque : en fonction de votre connecteur de base de données, le résultat renvoyé par cette méthode peut être géré manuellement en mémoire. Envisagez d'utiliser le wrapper #exec_query à la place." . execute est une méthode dangereuse qui peut provoquer des fuites de mémoire ou d'autres ressources.

1 votes

Wow, bonne prise ! Voici le documentation . On dirait aussi que l'adaptateur Postgres fuirait ici.

0 votes

Je me demande, si AR ne nous laisse rien pour on duplicate key update partout

5voto

leandroico Points 371

Vous devriez juste utiliser quelque chose comme :

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

Ça ferait l'affaire. En utilisant le ActiveRecord::Base#send pour invoquer la méthode assainir_sql_pour_assignation fait en sorte que Ruby (du moins la version 1.8.7) ne tienne pas compte du fait que l'option assainir_sql_pour_assignation est en fait une méthode protégée.

2voto

Tg Vikor Points 81

Parfois, il serait préférable d'utiliser le nom de la classe parent au lieu du nom de la table :

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

Par exemple, la classe de base "Personne", les sous-classes (et tables de base de données) "Client" et "Vendeur". Au lieu d'utiliser :

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

Vous pouvez utiliser l'objet de la classe de base de cette façon :

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

-12voto

donvnielsen Points 11

J'ai dû utiliser du sql brut car je n'ai pas réussi à faire fonctionner composite_primary_keys avec activerecord 2.3.8. Donc pour accéder à la table sqlserver 2000 avec une clé primaire composite, il fallait utiliser 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

Si une meilleure solution est disponible, veuillez la partager.

14 votes

Sql-injection sont possibles ici !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X