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

98

Je veux exécuter une mise à jour de sql brut 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 des paramètres dynamiques dans la méthode.

Quelqu'un pourrait-il m'aider à ce sujet?

Ywenbo
la source
Pourquoi voulez-vous faire cela en utilisant SQL brut, le but d'ActiveRecord est de vous aider à éviter cela ...
Andrew
si j'utilise AR, je dois d'abord obtenir l'objet modèle par la méthode de recherche d'AR avec le champ id, puis effectuer l'opération de mise à jour. Donc, du point de vue des opérations, un UPDATE AR a besoin de deux sqls avec base de données; d'un autre côté, je ne suis pas sûr que la méthode de mise à jour d'AR utilise la liaison dynamique. donc je veux utiliser raw sql avec liaison dynamique pour une interaction avec db pour l'opération de mise à jour, mais je ne sais pas comment passer des paramètres pour remplacer le? dans SQL par AR.
ywenbo
27
Il y a de nombreuses raisons valables de faire cela. Premièrement, la requête peut être trop complexe pour être traduite en utilisant la méthode Ruby régulière ... deuxièmement, les paramètres peuvent avoir des caractères spéciaux comme% ou des guillemets, et c'est une douleur dans le cul d'échapper au ..
Henley Chiu
3
@Andrew, il vaut mieux utiliser les fonctions brutes de mysql que d'accepter la "commodité" qu'offre AR.
Vert
4
@Green pas si vous souhaitez déplacer votre application de MySQL vers PostgreSQL ou autre chose. L'un des principaux points d'un ORM est de rendre votre application portable.
Andrew

Réponses:

102

Il ne semble pas que l'API Rails expose des méthodes pour le faire 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 à faire cela (connexions laissées ouvertes, etc.). Je tracerais 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 à moins que vous ne le fassiez un million de fois de suite, vous feriez probablement mieux de simplement 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.

Brian Deterling
la source
26
Méfiez-vous de la méthode suggérée de "substitution Ruby normale" field=#{value}car elle vous laisse grand ouvert aux attaques par injection SQL. Si vous suivez ce chemin, consultez le module ActiveRecord :: ConnectionAdapters :: Quoting .
Paul Annesley
3
Veuillez noter que mysql2 ne prend pas en charge les instructions préparées, voir aussi: stackoverflow.com/questions/9906437
...
1
@reto On dirait qu'ils sont proches: github.com/brianmario/mysql2/pull/289
Brian Deterling
3
Aucune preparedéclaration dans Mysql2
Vert
1
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. Pensez à utiliser le wrapper #exec_query à la place." . executeest une méthode dangereuse et peut provoquer des fuites de mémoire ou d'autres ressources.
kolen
32

ActiveRecord::Base.connectiona une quoteméthode qui prend une valeur de chaîne (et éventuellement l'objet de 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

MISE À JOUR: comme @kolen le souligne, vous devriez utiliser à la exec_updateplace. Cela gérera les citations pour vous et évitera également les fuites de mémoire. La signature fonctionne un peu différemment cependant:

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 nilindiquer le type de colonne et Rails fera généralement la bonne chose.

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

Paul A Jungwirth
la source
3
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. Pensez à utiliser le wrapper #exec_query à la place." . executeest une méthode dangereuse et peut provoquer des fuites de mémoire ou d'autres ressources.
kolen le
1
Wow bonne prise! Voici la documentation . Il semble également que l'adaptateur Postgres fuit ici.
Paul A Jungwirth
on duplicate key update
Je me
1
@Shrinath puisque cette question concerne l'écriture de SQL brut, vous pouvez faire la requête ON CONFLICT DO UPDATEsi vous le souhaitez. Pour le SQL non brut, ce joyau semble pratique: github.com/jesjos/active_record_upsert
Paul A Jungwirth
4

Vous devriez simplement utiliser quelque chose comme:

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

Cela ferait l'affaire. Utiliser la méthode ActiveRecord :: Base # send pour invoquer sanitize_sql_for_assignment fait que Ruby (au moins la version 1.8.7) ignore le fait que sanitize_sql_for_assignment est en fait une méthode protégée.

leandroico
la source
2

Parfois, il serait préférable d'utiliser le nom de la classe parente 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, classe de base "Personne", sous-classes (et tables de base de données) "Client" et "Vendeur" à la place en utilisant:

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 manière:

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

Pourquoi utiliser du SQL brut pour cela?

Si vous avez un modèle à utiliser 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 (juste la table), vous pouvez créer un fichier et un modèle qui hériteront deActiveRecord::Base

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

et utilisez à nouveau wherela même chose que ci-dessus:

ToTenMilan
la source
2
C'est tellement moins efficace, n'est-ce pas. Cela n'échange-t-il pas une déclaration de mise à jour pour une sélection, n'apporte pas les enregistrements dans la mémoire des rails, met à jour chaque enregistrement et renvoie une déclaration de mise à jour pour chaque enregistrement. 1 mise à jour vs 1 par enregistrement et beaucoup de bande passante, etc.? La RA optimise-t-elle cela ou non? Je ne pense pas que ce soit le cas.
Jimi Kimble
0

Voici une astuce que j'ai récemment élaborée pour exécuter SQL brut avec des liaisons:

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

ApplicationRecorddéfinit ceci:

# 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

et cela est similaire à la façon dont AR lie ses propres requêtes.

écume
la source
-11

J'avais besoin d'utiliser raw sql car je n'avais pas réussi à faire fonctionner composite_primary_keys avec activerecord 2.3.8. Ainsi, pour accéder à la table sqlserver 2000 avec une clé primaire composite, un sql brut était requis.

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.

Donvnielsen
la source
10
sql-injection est possible ici!
mystdeim
-18

Dans Rails 3.1, vous devez utiliser l'interface de requête:

  • nouveau (attributs)
  • créer (attributs)
  • create! (attributs)
  • find (id_or_array)
  • détruire (id_or_array)
  • détruire_tous
  • supprimer (id_or_array)
  • delete_all
  • mise à jour (identifiants, mises à jour)
  • update_all (mises à jour)
  • existe?

update et update_all sont l'opération dont vous avez besoin.

Voir les détails ici: http://m.onkey.org/active-record-query-interface

activars
la source