web-dev-qa-db-fra.com

Comment exécuter une mise à jour sql brute avec une liaison dynamique dans rails

Je veux exécuter une mise à jour brute 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 passer les valeurs du paramètre dynamique à la méthode.

Est-ce que quelqu'un pourrait me donner de l'aide?

87
ywenbo

Il ne semble pas que l'API Rails expose les méthodes permettant d'effectuer cette opération de manière générique. Vous pouvez essayer d'accéder à la connexion sous-jacente et d'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 ramifications à cela (connexions laissées ouvertes, etc.). Je voudrais suivre le code Rails pour une mise à jour normale pour 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 si vous ne le faites pas un million de fois de suite, vous feriez probablement mieux de créer la mise à jour avec une substitution Ruby normale, par exemple.

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

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

95
Brian Deterling

ActiveRecord::Base.connection a une méthode quote qui prend une valeur de chaîne (et éventuellement l’objet column). Donc, vous pouvez dire ceci:

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

Notez que si vous êtes dans un objet de migration Rails ou ActiveRecord), vous pouvez le réduire à:

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

UPDATE: Comme l'indique @kolen, vous devez utiliser exec_update à la place. Cela gérera la citation pour vous et évitera également les fuites de mémoire. La signature fonctionne un peu différemment si:

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 habituellement ce qu'il faut.

Il y a aussi exec_query, exec_insert, et exec_delete, en fonction de vos besoins.

29
Paul A Jungwirth

Vous devriez juste utiliser quelque chose comme:

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

Cela ferait l'affaire. L'utilisation de la méthode ActiveRecord :: Base # send pour appeler sanitize_sql_for_assignment permet à Ruby (au moins la version 1.8.7) d'ignorer la fait que le sanitize_sql_for_assignment est en fait une méthode protégée.

4
leandroico

Il serait parfois préférable d'utiliser le nom de la classe parente plutôt que le 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, classe de base "Personne", sous-classes (et tables de base de données) "Client" et "Vendeur" à la place de:

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 un objet de la classe de base de cette manière:

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

Pourquoi utiliser du SQL brut pour cela?

Si vous avez un modèle pour cela utilisez 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 vous n'avez pas de modèle pour cela (uniquement le tableau), vous pouvez créer un fichier et un modèle qui hériteront de ActiveRecord::Base

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

et encore utiliser where comme ci-dessus:

1
ToTenMilan