Railsで動的バインディングを使用して生の更新SQLを実行する方法


98

以下のような1つの更新生SQLを実行したいと思います。

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

このSQLはによって実行されますがActiveRecord::Base.connection.execute、動的パラメーター値をメソッドに渡す方法がわかりません。

誰かが私にそれについて何か助けてもらえますか?


生のSQLを使用してこれを行うのはなぜですか?ActiveRecordの目的は、それを回避できるようにすることです...
Andrew

ARを使用する場合、最初にARのidフィールドを持つfindメソッドでモデルオブジェクトを取得し、次に更新操作を実行する必要があります。したがって、操作の観点から見ると、1つのUPDATE ARにはデータベースを持つ2つのSQLが必要です。一方、ARの更新メソッドが動的バインディングを使用するかどうかはわかりません。だから私は更新操作のためにデータベースとの1つの相互作用のために動的バインディングで生のSQLを使用したいのですが、パラメータを渡して置換する方法がわかりません?ARによるSQLで。
ywenbo

27
これを行うには多くの正当な理由があります。まず、クエリは通常のRubyの方法を使用してに変換するには複雑すぎるかもしれ... 2番目、パラメータは%、または引用符などの特殊文字を有していてもよく、それは...脱出するお尻の痛みだ
ヘンリーチウ

3
@ Andrew、ARが提供する「便利さ」を受け入れるよりも、生のmysql関数を使用する方が良いです。
グリーン

4
@Greenは、アプリをMySQLからPostgreSQLまたはその他に移動したい場合には使用できません。ORMの主なポイントの1つは、アプリを移植可能にすることです。
アンドリュー

回答:


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コードをトレースして、実際のクエリ以外に何が行われているかを確認します。

準備されたクエリを使用すると、データベースの時間を少し節約できますが、これを100万回続けて行わない限り、通常のRuby置換を使用して更新をビルドするだけのほうがよいでしょう。

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

または、コメンターが言ったようにActiveRecordを使用します。


26
field=#{value}SQLインジェクション攻撃の可能性が広がるため、推奨される「通常のRuby置換」方法に注意してください。そのパスをたどった場合は、ActiveRecord :: ConnectionAdapters :: Quotingモジュールを確認してください。
ポールアンズレー

3
してくださいノート、mysql2プリペアドステートメントをサポートしていません、以下を参照してください。stackoverflow.com/questions/9906437/...を
レト

1
@retoしかし、近いようです:github.com/brianmario/mysql2/pull/289
Brian Deterling

3
いいえprepareMysql2で声明ません
グリーン

1
ドキュメントによると:「注:データベースコネクタによっては、このメソッドによって返される結果は手動でメモリ管理される場合があります。代わりに#exec_queryラッパーの使用を検討してください。」execute危険な方法であり、メモリやその他のリソースリークを引き起こす可能性があります。
kolen

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

ここで最後のパラメータは、バインドパラメータを表すタプルの配列です。各タプルでは、​​最初のエントリは列のタイプで、2番目のエントリは値です。あなたnilは列のタイプを指定することができ、Railsは通常正しいことをします。

またexec_query、必要に応じて、、、exec_insertおよびexec_deleteがあります。


3
ドキュメントによると:「注:データベースコネクタによっては、このメソッドによって返される結果は手動でメモリ管理される場合があります。代わりに#exec_queryラッパーの使用を検討してください。」execute危険な方法であり、メモリやその他のリソースリークを引き起こす可能性があります。
kolen

1
すごいキャッチ!こちらがドキュメントです。また、Postgresアダプターがここでリークするようです。
Paul A Jungwirth

不思議なことに、ARがon duplicate key updateどこにでも何も残さない場合
Shrinath

1
@Shrinathこの質問は生のSQLの記述に関するON CONFLICT DO UPDATEものなので、必要に応じてクエリを作成できます。非raw SQLの場合、このgemは便利に見えます:github.com/jesjos/active_record_upsert
Paul A Jungwirth

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)

たとえば、「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)

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
これはあまり効率が悪いですよね。これは、selectの1つの更新ステートメントをトレードせず、レコードを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が独自のクエリをバインドする方法に似ています。


-10

composite_primary_keysをactiverecord 2.3.8で機能させるのに失敗したため、生のSQLを使用する必要がありました。したがって、複合主キーを使用して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

より良い解決策がある場合は、共有してください。


9
sql-injectionはここで可能です!
mystdeim 2013年

-17

Rails 3.1では、クエリインターフェイスを使用する必要があります。

  • 新規(属性)
  • create(attributes)
  • 作成!(属性)
  • find(id_or_array)
  • destroy(id_or_array)
  • destroy_all
  • delete(id_or_array)
  • すべて削除
  • update(ids、updates)
  • update_all(updates)
  • 存在する?

updateおよびupdate_allは、必要な操作です。

詳細はこちら:http : //m.onkey.org/active-record-query-interface

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.