如何在rails中使用动态绑定执行原始更新sql

Posted

技术标签:

【中文标题】如何在rails中使用动态绑定执行原始更新sql【英文标题】:How to execute a raw update sql with dynamic binding in rails 【发布时间】:2010-12-19 13:04:56 【问题描述】:

我想执行一个更新原始 sql,如下所示:

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

这条SQL会被ActiveRecord::Base.connection.execute执行,但是我不知道如何将动态参数值传递给方法。

有人可以帮我吗?

【问题讨论】:

为什么要使用原始 SQL 来执行此操作,ActiveRecord 的目的是帮助您避免这种情况... 如果我使用 AR,首先我应该通过 AR 的 find 方法和 id 字段获取模型对象,然后进行更新操作。因此,从操作的角度来看,一个 UPDATE AR 需要两个带有数据库的 sql;另一方面,我不确定 AR 的更新方法是否使用动态绑定。所以我想使用带有动态绑定的原始sql与db进行一次交互以进行更新操作,但我不知道如何传递参数来替换?由 AR 在 sql 中。 这样做有很多正当理由。首先,查询可能过于复杂,无法使用常规的 Ruby 方式进行转换……其次,参数可能包含特殊字符,例如 % 或引号,而转义... @Andrew,最好使用原始的 mysql 函数,而不是接受 AR 提供的“便利”。 @Green 如果您想将您的应用程序从 MySQL 移动到 PostgreSQL 或其他地方,请不要这样做。 ORM 的重点之一是使您的应用具有可移植性。 【参考方案1】:

看起来 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。

【讨论】:

当心建议的使用field=#value 的“普通 Ruby 替换”方法,因为这会让您很容易受到 SQL 注入攻击。如果您走这条路,请查看ActiveRecord::ConnectionAdapters::Quoting 模块。 请注意,mysql2不支持prepared statements,另见:***.com/questions/9906437/… @reto 看起来他们很接近:github.com/brianmario/mysql2/pull/289 Mysql2 中没有prepare 语句 根据文档:“注意:根据您的数据库连接器,此方法返回的结果可能是手动内存管理的。请考虑改用#exec_query 包装器。”execute 是一种危险的方法,可能导致内存或其他资源泄漏。【参考方案2】:

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_queryexec_insertexec_delete,这取决于你的需要。

【讨论】:

根据文档:“注意:根据您的数据库连接器,此方法返回的结果可能是手动内存管理的。请考虑改用#exec_query 包装器。”execute 是一种危险的方法,可能导致内存或其他资源泄漏。 哇好收获!这是documentation。而且看起来the Postgres adapter 会在这里泄漏。 想知道,如果 AR 让我们在 on duplicate key update 无处不在 @Shrinath 因为这个问题是关于编写原始 SQL 的,如果您愿意,可以查询 ON CONFLICT DO UPDATE。对于非原始 SQL,这个 gem 看起来很方便:github.com/jesjos/active_record_upsert【参考方案3】:

你应该使用类似的东西:

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 的事实strong> 实际上是一个受保护的方法。

【讨论】:

【参考方案4】:

有时使用父类的名称而不是表的名称会更好:

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

例如“Person”基类、子类(和数据库表)“Client”和“Seller” 而是使用:

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)

【讨论】:

【参考方案5】:

这是我最近为使用绑定执行原始 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 如何绑定自己的查询。

【讨论】:

【参考方案6】:

其他答案都没有告诉我如何使用命名参数,所以我最终将exec_updatesanitize_sql 结合起来:

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

这适用于我在 Rails 5 上,它执行以下 SQL:

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

如果没有,您需要使用现有的 Rails 模型而不是 User

当我使用?$1/$2 等时,我想使用命名参数来避免排序问题。当我有多个参数时,位置排序有点令人沮丧,但命名参数允许我重构 SQL 命令而无需更新参数。

【讨论】:

【参考方案7】:

我需要使用原始 sql,因为我无法让 Composite_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

如果有更好的解决方案,请分享。

【讨论】:

这里可以进行sql注入!【参考方案8】:

在 Rails 3.1 中,你应该使用查询接口:

新(属性) 创建(属性) 创建!(属性) 查找(id_or_array) 销毁(id_or_array) destroy_all 删除(id_or_array) delete_all 更新(ID、更新) update_all(更新) 存在吗?

update 和 update_all 是您需要的操作。

在此处查看详细信息:http://m.onkey.org/active-record-query-interface

【讨论】:

这么多反对票,没有 cmets。对不起,伙计。

以上是关于如何在rails中使用动态绑定执行原始更新sql的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Rails 中链接原始 SQL 查询或如何从 Rails 中的原始 SQL 查询返回 ActiveRecord_Relation?

sqlalchemy : 使用参数绑定执行原始 sql

Laravel 5 Eloquent:如何获取正在执行的原始 sql? (带有绑定数据)

Rails 3.x 如何根据行值编写全部更新?

如何使用 SQL Server 更新触发器中的原始值更改第二条记录

jQuery动态绑定