Jak wykonać surową aktualizację sql z dynamicznym wiązaniem w railsach

98

Chcę wykonać jedną aktualizację surowego sql, jak poniżej:

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

Ten kod SQL zostanie wykonany przez ActiveRecord::Base.connection.execute, ale nie wiem, jak przekazać wartości parametrów dynamicznych do metody.

Czy ktoś mógłby mi w tym pomóc?

ywenbo
źródło
Dlaczego chcesz to zrobić przy użyciu surowego kodu SQL, celem ActiveRecord jest pomoc w uniknięciu tego ...
Andrew
jeśli używam AR, najpierw powinienem pobrać Model Object metodą AR z polem id, a następnie wykonać operację aktualizacji. Więc z punktu widzenia operacji jeden UPDATE AR potrzebuje dwóch sqlów z bazą danych; z drugiej strony nie jestem pewien, czy metoda aktualizacji AR używa dynamicznego wiązania. więc chcę używać surowego sql z dynamicznym wiązaniem dla jednej interakcji z db do operacji aktualizacji, ale nie wiem, jak przekazać parametry, aby zastąpić? w sql przez AR.
ywenbo,
27
Jest wiele ważnych powodów, dla których warto to zrobić. Po pierwsze, zapytanie może być zbyt skomplikowane, aby można je było przetłumaczyć na zwykły sposób w Rubim ... po drugie, parametry mogą zawierać znaki specjalne, takie jak% lub cudzysłowy, a ucieczka przed ...
Henley Chiu
3
@ Andrew, lepiej jest używać surowych funkcji mysql niż akceptować „wygodę”, jaką oferuje AR.
Green
4
@Green nie, jeśli kiedykolwiek zechcesz przenieść swoją aplikację z MySQL do PostgreSQL lub czegoś innego. Jednym z głównych punktów ORM jest zapewnienie przenośności aplikacji.
Andrew,

Odpowiedzi:

102

Wygląda na to, że API Railsów nie ujawnia metod służących do tego w sposób ogólny. Możesz spróbować uzyskać dostęp do podstawowego połączenia i użyć jego metod, np. Dla MySQL:

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

Nie jestem pewien, czy istnieją inne konsekwencje takiego działania (pozostawione otwarte połączenia itp.). Prześledziłbym kod Railsów dla normalnej aktualizacji, aby zobaczyć, co robi poza faktycznym zapytaniem.

Korzystanie z przygotowanych zapytań może zaoszczędzić ci trochę czasu w bazie danych, ale jeśli nie robisz tego milion razy z rzędu, prawdopodobnie lepiej byłoby po prostu zbudować aktualizację z normalnym podstawieniem Rubiego, np.

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

lub używając ActiveRecord, jak powiedzieli komentatorzy.

Brian Deterling
źródło
26
Uważaj na sugerowaną metodę „normalnego zastępowania Rubiego”, field=#{value}ponieważ otwiera to Cię na ataki typu SQL injection. Jeśli pójdziesz tą ścieżką, sprawdź moduł ActiveRecord :: ConnectionAdapters :: Quoting .
Paul Annesley,
3
Uwaga, mysql2 nie obsługuje przygotowanych instrukcji, zobacz też: stackoverflow.com/questions/9906437/ ...
reto
1
@reto Wygląda na to, że są blisko: github.com/brianmario/mysql2/pull/289
Brian Deterling
3
Brak preparestwierdzenia w Mysql2
Green
1
Zgodnie z dokumentacją: „Uwaga: w zależności od łącznika bazy danych wynik zwracany przez tę metodę może być ręcznie zarządzany pamięcią. Rozważ zamiast tego użycie opakowania #exec_query”. . executejest metodą niebezpieczną i może powodować wycieki pamięci lub innych zasobów.
kolen
32

ActiveRecord::Base.connectionma quotemetodę, która przyjmuje wartość ciągu (i opcjonalnie obiekt kolumny). Możesz więc powiedzieć tak:

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

Zauważ, że jeśli jesteś w migracji Railsów lub w obiekcie ActiveRecord, możesz skrócić to do:

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

AKTUALIZACJA: Jak wskazuje @kolen, powinieneś użyć exec_updatezamiast tego. Spowoduje to wycenę za Ciebie, a także pozwoli uniknąć wycieku pamięci. Podpis działa jednak nieco inaczej:

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

Tutaj ostatni parametr to tablica krotek reprezentujących parametry wiązania. W każdej krotce pierwszy wpis to typ kolumny, a drugi to wartość. Możesz podać niltyp kolumny, a jednak Railsy zwykle postępują właściwie.

Istnieją również exec_query, exec_inserti exec_delete, w zależności od tego, czego potrzebujesz.

Paul A Jungwirth
źródło
3
Zgodnie z dokumentacją: „Uwaga: w zależności od łącznika bazy danych wynik zwracany przez tę metodę może być ręcznie zarządzany pamięcią. Rozważ zamiast tego użycie opakowania #exec_query”. . executejest metodą niebezpieczną i może powodować wycieki pamięci lub innych zasobów.
kolen
1
Wow, dobry chwyt! Oto dokumentacja . Wygląda też na to, że adapter Postgres przeciekałby tutaj.
Paul A Jungwirth,
Ciekawe, czy AR nie pozostawia nam nic do on duplicate key updatewszystkiego
Shrinath
1
@Shrinath, ponieważ to pytanie dotyczy pisania surowego kodu SQL, możesz wykonać zapytanie, ON CONFLICT DO UPDATEjeśli chcesz. W przypadku nie-surowego kodu
Paul A Jungwirth
4

Powinieneś po prostu użyć czegoś takiego:

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

To wystarczy. Użycie metody ActiveRecord :: Base # send do wywołania sanitize_sql_for_assignment sprawia, że ​​Ruby (przynajmniej wersja 1.8.7) pomija fakt, że sanitize_sql_for_assignment jest w rzeczywistości metodą chronioną.

leandroico
źródło
2

Czasami lepiej byłoby użyć nazwy klasy nadrzędnej zamiast nazwy tabeli:

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

Na przykład klasa bazowa „Osoba”, podklasy (i tabele bazy danych) „Klient” i „Sprzedawca” Zamiast tego użyj:

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

Możesz użyć obiektu klasy bazowej w następujący sposób:

person.class.unscoped.where(self.class.primary_key => id).update_all(created _at: timestamp)
mikowiec
źródło
1

Po co w tym celu używać surowego SQL?

Jeśli masz model do tego użyj 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

Jeśli nie masz dla niego modelu (tylko tabela), możesz utworzyć plik i model, które będą dziedziczyć poActiveRecord::Base

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

i ponownie użyj wheretego samego co powyżej:

ToTenMilan
źródło
2
To jest o wiele mniej wydajne, prawda? Czy to nie zamienia jednej instrukcji aktualizacji na wybór, nie wprowadza rekordów do pamięci szyn, aktualizuje każdy rekord i nie wysyła instrukcji aktualizacji dla każdego rekordu. 1 aktualizacja vs 1 na rekord i dużo przepustowości itp.? AR optymalizuje to, czy nie? Myślę, że tak nie jest.
Jimi Kimble,
0

Oto sztuczka, którą niedawno opracowałem, dotyczącą wykonywania surowego sql z powiązaniami:

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

gdzie ApplicationRecordto określa:

# 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

i to jest podobne do tego, jak AR wiąże swoje własne zapytania.

piana
źródło
-11

Musiałem użyć surowego sql, ponieważ nie udało mi się uzyskać composite_primary_keys do działania z activerecord 2.3.8. Tak więc, aby uzyskać dostęp do tabeli sqlserver 2000 ze złożonym kluczem podstawowym, wymagany był surowy 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

Jeśli dostępne jest lepsze rozwiązanie, udostępnij.

donvnielsen
źródło
11
sql-injection są możliwe tutaj!
mystdeim
-19

W Railsach 3.1 powinieneś używać interfejsu zapytań:

  • nowy (atrybuty)
  • utwórz (atrybuty)
  • utwórz! (atrybuty)
  • znajdź (id_or_array)
  • zniszcz (id_or_array)
  • zniszcz_wszystkie
  • usuń (id_or_array)
  • Usuń wszystko
  • aktualizacja (identyfikatory, aktualizacje)
  • update_all (aktualizacje)
  • istnieje?

update i update_all to operacja, której potrzebujesz.

Zobacz szczegóły tutaj: http://m.onkey.org/active-record-query-interface

aktywatorzy
źródło