Как выполнить необработанное обновление sql с динамической привязкой в ​​рельсах


98

Я хочу выполнить одно обновление raw sql, как показано ниже:

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

Этот SQL будет выполняться ActiveRecord::Base.connection.execute, но я не знаю, как передать значения динамических параметров в метод.

Может ли кто-нибудь помочь мне в этом?


Почему вы хотите делать это, используя необработанный SQL, цель ActiveRecord - помочь вам избежать этого ...
Эндрю,

если я использую AR, сначала я должен получить объект модели с помощью метода поиска AR с полем id, а затем выполнить операцию обновления. Таким образом, с точки зрения операций для одного UPDATE AR нужны два sql с базой данных; с другой стороны, я не уверен, что метод обновления AR использует динамическую привязку. поэтому я хочу использовать необработанный sql с динамической привязкой для одного взаимодействия с db для операции обновления, но я не знаю, как передать параметры для замены? в sql от AR.
ywenbo

27
Для этого есть много веских причин. Во-первых, запрос может быть слишком сложным, чтобы его можно было преобразовать в обычный метод Ruby ... во-вторых, в параметрах могут быть специальные символы, такие как% или кавычки, и избежать этого
Хенли Чиу,

3
@Andrew, лучше использовать необработанные функции mysql, чем принимать "удобство", которое предлагает AR.
Грин

4
@Green нет, если вы когда-нибудь захотите перенести свое приложение с MySQL на PostgreSQL или что-то еще. Одна из основных задач ORM - сделать ваше приложение переносимым.
Эндрю

Ответы:


102

Не похоже, что Rails API предоставляет общие методы для этого. Вы можете попробовать получить доступ к базовому соединению и использовать его методы, например, для MySQL:

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

Я не уверен, есть ли у этого другие последствия (соединения остаются открытыми и т. Д.). Я бы проследил код Rails для обычного обновления, чтобы увидеть, что он делает помимо фактического запроса.

Использование подготовленных запросов может сэкономить вам небольшое количество времени в базе данных, но если вы не делаете это миллион раз подряд, вам, вероятно, будет лучше просто создать обновление с обычной заменой Ruby, например

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

или с помощью ActiveRecord, как сказали комментаторы.


26
Остерегайтесь предлагаемого метода «нормальной замены Ruby», field=#{value}поскольку это оставляет вас открытыми для атак SQL-инъекций. Если вы пойдете по этому пути, проверьте модуль ActiveRecord :: ConnectionAdapters :: Quoting .
Пол Аннесли

3
Обратите внимание, mysql2 не поддерживает подготовленные операторы, см. Также: stackoverflow.com/questions/9906437/…
reto

1
@reto Похоже, они близки: github.com/brianmario/mysql2/pull/289
Брайан Детерлинг,

3
Нет prepareзаявления в Mysql2
Green

1
Согласно документации: «Примечание. В зависимости от коннектора базы данных результат, возвращаемый этим методом, может управляться памятью вручную. Вместо этого рассмотрите возможность использования оболочки #exec_query». . executeявляется опасным методом и может вызвать утечку памяти или других ресурсов.
Колен

32

ActiveRecord::Base.connectionимеет quoteметод, который принимает строковое значение (и, возможно, объект столбца). Итак, вы можете сказать это:

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

Обратите внимание, если вы выполняете миграцию Rails или объект ActiveRecord, вы можете сократить это до:

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

ОБНОВЛЕНИЕ: как указывает @kolen, вы должны использовать exec_updateвместо этого. Это обработает цитирование за вас, а также предотвратит утечку памяти. Однако подпись работает немного иначе:

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

Здесь последний параметр - это массив кортежей, представляющих параметры привязки. В каждом кортеже первая запись - это тип столбца, а вторая - значение. Вы можете nilуказать тип столбца, и Rails обычно поступает правильно.

Есть также exec_query, exec_insertи exec_delete, в зависимости от того, что вам нужно.


3
Согласно документации: «Примечание. В зависимости от коннектора базы данных результат, возвращаемый этим методом, может управляться памятью вручную. Вместо этого рассмотрите возможность использования оболочки #exec_query». . executeявляется опасным методом и может вызвать утечку памяти или других ресурсов.
Колен

1
Вау хороший улов! Вот документация . Также похоже, что здесь протечет адаптер Postgres .
Пол А Юнгвирт

Удивление, если AR оставляет нас ни с чем on duplicate key updateвезде
Shrinath

1
@Shrinath, поскольку этот вопрос касается написания необработанного SQL, вы можете сделать запрос, ON CONFLICT DO UPDATEесли хотите. Для не-сырого SQL этот гем выглядит удобно: github.com/jesjos/active_record_upsert
Paul A

4

Вы должны просто использовать что-то вроде:

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

Это поможет. Использование метода ActiveRecord :: Base # send для вызова sanitize_sql_for_assignment заставляет Ruby (по крайней мере, версия 1.8.7) пропускать тот факт, что sanitize_sql_for_assignment на самом деле является защищенным методом.


2

Иногда было бы лучше использовать имя родительского класса вместо имени таблицы:

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

Например, базовый класс «Человек», подклассы (и таблицы базы данных) «Клиент» и «Продавец» Вместо использования:

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

Вы можете использовать объект базового класса следующим образом:

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

1

Зачем использовать для этого необработанный SQL?

Если у вас есть модель, используйте 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

Если у вас нет модели для него (только таблица), вы можете создать файл и модель, которые будут унаследованы отActiveRecord::Base

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

и снова используйте whereто же, что и выше:


2
Это намного менее эффективно, не правда ли? Разве это не меняет один оператор обновления для выбора, переносит записи в память rails, обновляет каждую запись и отправляет обратно оператор обновления для каждой записи. 1 обновление против 1 на запись и большая пропускная способность и т. Д.? Оптимизирует ли это AR или нет? Я не думаю, что это так.
Джими Кимбл

0

Вот трюк, который я недавно разработал для выполнения необработанного sql с привязками:

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

где ApplicationRecordэто определяется:

# 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

и это похоже на то, как AR связывает свои собственные запросы.


-11

Мне нужно было использовать raw sql, потому что мне не удалось заставить композитный_primary_keys работать с activerecord 2.3.8. Поэтому для доступа к таблице sqlserver 2000 с составным первичным ключом требовался необработанный 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

Если доступно лучшее решение, поделитесь.


11
sql-инъекция возможна здесь!
mystdeim

-19

В Rails 3.1 вы должны использовать интерфейс запросов:

  • новый (атрибуты)
  • создать (атрибуты)
  • создать! (атрибуты)
  • найти (id_or_array)
  • уничтожить (id_or_array)
  • destroy_all
  • удалить (id_or_array)
  • удалить все
  • обновление (идентификаторы, обновления)
  • update_all (обновления)
  • существуют?

update и update_all - необходимые вам операции.

Подробности смотрите здесь: http://m.onkey.org/active-record-query-interface

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.