How to execute a raw update sql with dynamic binding in rails
Asked Answered
C

8

111

I want to execute one update raw sql like below:

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

This SQL will be executed by ActiveRecord::Base.connection.execute, but I don't know how to pass the dynamic parameter values into the method.

Could someone give me any help on it?

Crosswind answered 19/12, 2010 at 13:4 Comment(6)
Why do you want to do this using raw SQL, the point of ActiveRecord is to help you avoid that...Shyamal
if i use AR, firstly i should get Model Object by AR's find method with id field, then to do update operation. So from the view of operations one UPDATE AR need two sqls with database; on the other hand i am not sure the update method of AR uses dynamic binding. so i want to use raw sql with dynamic binding for one interaction with db for update operation, but i don't know how to pass parameters to replace the ? in sql by AR.Crosswind
There's many valid reasons for doing this. First, the query may be too complex to translate into using the regular Ruby way... second, the parameters may have special characters like %, or quotes, and it's a pain in the ass to escape the..Siffre
@Andrew, it is better to use raw mysql functions than accept the "convenience" that AR offers.Thremmatology
@Thremmatology not if you ever want to move your app from MySQL to PostgreSQL or something else. One of the major points of an ORM is to make your app portable.Shyamal
@Shyamal One word: migrations. During a migration you can't rely on ActiveRecord convenience.Alcalde
C
115

It doesn't look like the Rails API exposes methods to do this generically. You could try accessing the underlying connection and using it's methods, e.g. for MySQL:

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

I'm not sure if there are other ramifications to doing this (connections left open, etc). I would trace the Rails code for a normal update to see what it's doing aside from the actual query.

Using prepared queries can save you a small amount of time in the database, but unless you're doing this a million times in a row, you'd probably be better off just building the update with normal Ruby substitution, e.g.

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

or using ActiveRecord like the commenters said.

Chamorro answered 19/12, 2010 at 18:54 Comment(16)
:-), appreciate you help very much, you're right, if you have some updates after tracing rails code please let me know. If really there is no exposes your provided method will be the best way for my case. My worry is that the update method of ActiveRecord doesn't use dynamic binding, if really so that's bad.Crosswind
It looks like you should call free on any results you get back, but I don't think that applies to an update. As long as you close the statement like I showed, you should be fine.Chamorro
really really appreciate your reply for my problems, thank you, up to now it's clear with your help.Crosswind
Beware the suggested "normal Ruby substitution" method using field=#{value} as this leaves you wide open to SQL injection attacks. If you go down that path, check out the ActiveRecord::ConnectionAdapters::Quoting module.Marquess
Agree with Paul. I updated the answer to include sanitizing the parameters.Chamorro
Please note, mysql2 does not support prepared statements, see also: #9906937Pastelist
@Pastelist Looks like they are close though: github.com/brianmario/mysql2/pull/289Chamorro
No prepare statement in Mysql2Thremmatology
And mysql2 now supports prepared statements: github.com/brianmario/mysql2/releases/tag/0.4.0Chamorro
You can use each to iterate over the results or call to_a just like with a normal non-prepared result.Chamorro
According to documentation: "Note: depending on your database connector, the result returned by this method may be manually memory managed. Consider using the #exec_query wrapper instead.". execute is dangerous method and can cause memory or other resource leaks.Germanium
The code given here seems wrong because prepare takes at least two arguments: the statement's name and the query.Neurosurgery
Using prepared queries can save you a small amount of time in the database, ... note that in some databases, namely Oracle, bind variables are absolutely the way to go, and can bring enormous benefits. It's not just about a "small amount of time", but also about saving space, re-using resources, making debugging easier (less stuff to go through in the data dictionary when looking at SQL plans) and others.Tavi
@Nowhereman I just tested it under Rails 6 and it works as shown in the answer.Chamorro
@Tavi agree that prepared statements are generally better, but if you are only executing once, performance might be better without explicitly preparing. This was a Rails question and there is some penalty for dropping out of normal ActiveRecord mode and accessing the raw connection, so it's up to the developer to determine if the benefits of prepared statements outweigh that cost.Chamorro
@BrianDeterling, yes, I think it's a shame that Rails doesn't support bind variables as first class citizen. It certainly isn't worth it dropping out of "Rails mode" in general, considering developing and maintenance cost.Tavi
C
38

ActiveRecord::Base.connection has a quote method that takes a string value (and optionally the column object). So you can say this:

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

Note if you're in a Rails migration or an ActiveRecord object you can shorten that to:

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

UPDATE: As @kolen points out, you should use exec_update instead. This will handle the quoting for you and also avoid leaking memory. The signature works a bit differently though:

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

Here the last param is a array of tuples representing bind parameters. In each tuple, the first entry is the column type and the second is the value. You can give nil for the column type and Rails will usually do the right thing though.

There are also exec_query, exec_insert, and exec_delete, depending on what you need.

Cecilia answered 1/7, 2014 at 23:1 Comment(4)
According to documentation: "Note: depending on your database connector, the result returned by this method may be manually memory managed. Consider using the #exec_query wrapper instead.". execute is dangerous method and can cause memory or other resource leaks.Germanium
Wow good catch! Here is the documentation. Also it looks like the Postgres adapter would leak here.Cecilia
Wonder, if AR leaves us with nothing for on duplicate key update everywhereCharentemaritime
@Charentemaritime since this question is about writing raw SQL you could make the query ON CONFLICT DO UPDATE if you like. For non-raw SQL this gem looks handy: github.com/jesjos/active_record_upsertCecilia
I
8

None of the other answers showed me how to use named parameters, so I ended up combining exec_update with sanitize_sql:

User.connection.exec_update(
  User.sanitize_sql(
    [
      "update users set name = :name where id = :id and name <> :name",
      {
        id: 123,
        name: 'My Name'
      }
    ]
  )
)

This works for me on Rails 5, and it executes this SQL:

update users set name = 'My Name' where id = 123 and name <> 'My Name'

You need to use an existing Rails model instead of User if you don't have that.

I wanted to use named parameters to avoid issues with the ordering when I use ? or $1/$2,etc. Positional ordering is kind of frustrating when I have more than a handful of parameters, but named parameters allow me to refactor the SQL command without having to update the parameters.

Indurate answered 27/10, 2021 at 8:21 Comment(0)
I
5

You should just use something like:

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

That would do the trick. Using the ActiveRecord::Base#send method to invoke the sanitize_sql_for_assignment makes the Ruby (at least the 1.8.7 version) skip the fact that the sanitize_sql_for_assignment is actually a protected method.

Inclement answered 15/5, 2012 at 3:12 Comment(0)
E
3

Sometime would be better use name of parent class instead name of table:

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

For example "Person" base class, subclasses (and database tables) "Client" and "Seller" Instead using:

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

You can use object of base class by this way:

person.class.unscoped.where(self.class.primary_key => id).update_all(created _at: timestamp)
Escaut answered 29/11, 2013 at 20:58 Comment(0)
A
1

Here's a trick I recently worked out for executing raw sql with binds:

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

where ApplicationRecord defines this:

# 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

and that is similar to how AR binds its own queries.

Abstention answered 14/5, 2020 at 17:20 Comment(0)
E
-12

I needed to use raw sql because I failed at getting composite_primary_keys to function with activerecord 2.3.8. So in order to access the sqlserver 2000 table with a composite primary key, raw sql was required.

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

If a better solution is available, please share.

Eudemon answered 25/4, 2013 at 18:6 Comment(1)
sql-injection are possible here!Leges
C
-23

In Rails 3.1, you should use the query interface:

  • new(attributes)
  • create(attributes)
  • create!(attributes)
  • find(id_or_array)
  • destroy(id_or_array)
  • destroy_all
  • delete(id_or_array)
  • delete_all
  • update(ids, updates)
  • update_all(updates)
  • exists?

update and update_all are the operation you need.

See details here: http://m.onkey.org/active-record-query-interface

Congeries answered 11/11, 2011 at 9:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.