So führen Sie ein Raw-Update-SQL mit dynamischer Bindung in Rails aus

98

Ich möchte ein Update Raw SQL wie folgt ausführen:

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

Diese SQL wird von ausgeführt ActiveRecord::Base.connection.execute, aber ich weiß nicht, wie ich die dynamischen Parameterwerte an die Methode übergeben soll.

Könnte mir jemand dabei helfen?

Ywenbo
quelle
Warum möchten Sie dies mit Raw SQL tun? Der Sinn von ActiveRecord besteht darin, Ihnen dabei zu helfen, dies zu vermeiden ...
Andrew
Wenn ich AR verwende, sollte ich zuerst das Modellobjekt mit der Suchmethode von AR mit dem ID-Feld abrufen und dann den Aktualisierungsvorgang ausführen. Aus Sicht der Operationen benötigt ein UPDATE AR zwei SQLs mit Datenbank. Andererseits bin ich mir nicht sicher, ob die Aktualisierungsmethode von AR eine dynamische Bindung verwendet. Ich möchte also Raw SQL mit dynamischer Bindung für eine Interaktion mit DB für den Aktualisierungsvorgang verwenden, aber ich weiß nicht, wie ich Parameter übergeben soll, um das zu ersetzen. in sql von AR.
Ywenbo
27
Dafür gibt es viele triftige Gründe. Erstens ist die Abfrage möglicherweise zu komplex, um sie auf die normale Ruby-Art zu übersetzen. Zweitens können die Parameter Sonderzeichen wie% oder Anführungszeichen enthalten, und es ist ein Schmerz im Arsch, dem ..
Henley Chiu
3
@ Andrew, es ist besser, rohe MySQL-Funktionen zu verwenden, als die "Bequemlichkeit" zu akzeptieren, die AR bietet.
Grün
4
@Green nicht, wenn Sie Ihre App jemals von MySQL auf PostgreSQL oder etwas anderes verschieben möchten. Einer der Hauptpunkte eines ORM ist es, Ihre App portabel zu machen.
Andrew

Antworten:

102

Es sieht nicht so aus, als würde die Rails-API Methoden bereitstellen, um dies generisch zu tun. Sie können versuchen, auf die zugrunde liegende Verbindung zuzugreifen und deren Methoden zu verwenden, z. B. für MySQL:

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

Ich bin mir nicht sicher, ob dies andere Konsequenzen hat (offene Verbindungen usw.). Ich würde den Rails-Code für ein normales Update verfolgen, um zu sehen, was es abgesehen von der eigentlichen Abfrage tut.

Durch die Verwendung vorbereiteter Abfragen können Sie ein wenig Zeit in der Datenbank sparen. Wenn Sie dies jedoch nicht millionenfach hintereinander tun, ist es wahrscheinlich besser, das Update nur mit normaler Ruby-Substitution zu erstellen, z

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

oder ActiveRecord verwenden, wie die Kommentatoren sagten.

Brian Deterling
quelle
26
Beachten Sie die vorgeschlagene Methode der "normalen Ruby-Substitution", field=#{value}da Sie dadurch offen für SQL-Injection-Angriffe sind. Wenn Sie diesen Pfad beschreiten, überprüfen Sie das Modul ActiveRecord :: ConnectionAdapters :: Quoting .
Paul Annesley
3
Bitte beachten Sie, dass mysql2 keine vorbereiteten Anweisungen unterstützt, siehe auch: stackoverflow.com/questions/9906437/…
reto
1
@reto Sieht aus wie sie sind in der Nähe: github.com/brianmario/mysql2/pull/289
Brian Deterling
3
Keine prepareAussage in MySQL2
Grün
1
In der Dokumentation heißt es: "Hinweis: Abhängig von Ihrem Datenbankconnector kann das von dieser Methode zurückgegebene Ergebnis manuell vom Speicher verwaltet werden. Verwenden Sie stattdessen den Wrapper #exec_query." . executeist eine gefährliche Methode und kann Speicher- oder andere Ressourcenlecks verursachen.
Kolen
32

ActiveRecord::Base.connectionhat eine quoteMethode, die einen Zeichenfolgenwert (und optional das Spaltenobjekt) annimmt. Sie können also Folgendes sagen:

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

Hinweis: Wenn Sie sich in einer Rails-Migration oder einem ActiveRecord-Objekt befinden, können Sie dies auf Folgendes verkürzen:

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

UPDATE: Wie @kolen hervorhebt, sollten Sie exec_updatestattdessen verwenden. Dadurch wird das Angebot für Sie erledigt und es wird auch ein Speicherverlust vermieden. Die Signatur funktioniert allerdings etwas anders:

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

Hier ist der letzte Parameter ein Array von Tupeln, die Bindungsparameter darstellen. In jedem Tupel ist der erste Eintrag der Spaltentyp und der zweite der Wert. Sie können nilfür den Spaltentyp angeben, und Rails wird normalerweise das Richtige tun.

Es gibt auch exec_query, exec_insertund exec_delete, je nachdem, was Sie brauchen.

Paul A Jungwirth
quelle
3
In der Dokumentation heißt es: "Hinweis: Abhängig von Ihrem Datenbankconnector kann das von dieser Methode zurückgegebene Ergebnis manuell vom Speicher verwaltet werden. Verwenden Sie stattdessen den Wrapper #exec_query." . executeist eine gefährliche Methode und kann Speicher- oder andere Ressourcenlecks verursachen.
Kolen
1
Wow guter Fang! Hier ist die Dokumentation . Es sieht auch so aus, als würde der Postgres-Adapter hier auslaufen.
Paul A Jungwirth
Wonder, wenn AR lässt uns mit nichts für on duplicate key updateüberall
Shrinath
1
@Shrinath Da es bei dieser Frage um das Schreiben von Roh-SQL geht, können Sie die Abfrage durchführen, ON CONFLICT DO UPDATEwenn Sie möchten . Für nicht-rohes SQL sieht dieses Juwel praktisch aus: github.com/jesjos/active_record_upsert
Paul A Jungwirth
4

Sie sollten nur etwas verwenden wie:

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

Das würde den Trick machen. Mit Hilfe der Activerecord :: Base # send Methode , um die aufzurufen sanitize_sql_for_assignment macht Ruby (zumindest die 1.8.7 Version) überspringen Sie die Tatsache , dass das sanitize_sql_for_assignment tatsächlich eine geschützte Methode.

Leandroico
quelle
2

Manchmal wäre es besser, den Namen der übergeordneten Klasse anstelle des Tabellennamens zu verwenden:

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

Zum Beispiel "Person" Basisklasse, Unterklassen (und Datenbanktabellen) "Client" und "Verkäufer" Verwenden Sie stattdessen:

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

Sie können das Objekt der Basisklasse folgendermaßen verwenden:

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

Warum dafür Raw SQL verwenden?

Wenn Sie ein Modell dafür haben, verwenden Sie 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

Wenn Sie kein Modell dafür haben (nur die Tabelle), können Sie eine Datei und ein Modell erstellen, von denen geerbt wirdActiveRecord::Base

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

und wieder wheredas gleiche wie oben verwenden:

ToTenMilan
quelle
2
Das ist so viel weniger effizient, nicht wahr? Tauscht dies nicht eine Aktualisierungsanweisung gegen eine Auswahl aus, bringt die Datensätze in den Rails-Speicher, aktualisiert jeden Datensatz und sendet eine Aktualisierungsanweisung für jeden Datensatz zurück. 1 Update gegen 1 pro Datensatz und viel Bandbreite usw.? Optimiert AR dies oder nicht? Ich glaube nicht.
Jimi Kimble
0

Hier ist ein Trick, den ich kürzlich für die Ausführung von unformatiertem SQL mit Bindungen ausgearbeitet habe:

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

wo ApplicationRecorddefiniert dies:

# 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

und das ist ähnlich wie AR seine eigenen Abfragen bindet.

Gischt
quelle
-10

Ich musste Raw SQL verwenden, weil ich nicht konnte, dass Composite_primary_keys mit Activerecord 2.3.8 funktioniert. Um mit einem zusammengesetzten Primärschlüssel auf die Tabelle sqlserver 2000 zugreifen zu können, war rohes SQL erforderlich.

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

Wenn eine bessere Lösung verfügbar ist, teilen Sie diese bitte mit.

donvnielsen
quelle
9
SQL-Injektion sind hier möglich!
Mystdeim
-17

In Rails 3.1 sollten Sie die Abfrageoberfläche verwenden:

  • neu (Attribute)
  • erstellen (Attribute)
  • erstellen! (Attribute)
  • find (id_or_array)
  • zerstören (id_or_array)
  • destroy_all
  • löschen (id_or_array)
  • alles löschen
  • Update (IDs, Updates)
  • update_all (Updates)
  • existiert?

update und update_all sind die Operationen, die Sie benötigen.

Details finden Sie hier: http://m.onkey.org/active-record-query-interface

Aktivare
quelle