QA@IT

ActiveRecordのクエリでカラム名をplaceholderのように安全に展開する方法はありませんか?

4197 PV

例えば

SomeModel.where("NULLIF(?, '') IS NOT NULL", :column_name)

というようなことができないかと思ったのですが、placeholderは値として展開されてしまい

SELECT * FROM "some_models" WHERE (NULLIF("column_name", '') IS NOT NULL)

クォートされてしまうので期待したような結果とならずに困っています。自前でsanitizeして文字列展開は最終手段としてあるのは承知していますが、もっと別の適切な方法があれば教えてください。

回答

Arel::Tableを使ってこんな感じでどうでしょうか?

  scope :not_blank, ->(column_name) { where "NULLIF(#{arel_table[column_name].name}, '') IS NOT NULL" }

> puts User.not_blank('name').to_sql
=> SELECT "users".* FROM "users"  WHERE (NULLIF(name, '') IS NOT NULL)

ところが、Oh! Cool!と思いきや、こいつザルでした…。

puts User.not_blank('hoge;fuga').to_sql
SELECT "users".* FROM "users"  WHERE (NULLIF(hoge;fuga, '') IS NOT NULL)

さて、@moro さんのおっしゃるように、単にエスケープするのでは「安全に」という要件を満たしているだけのコードでしかなく、「クールさにかけ」ます。気持ちの入ってないコードですね。
といって、特にクールなソリューションがあるわけでもないのですが、自分ならとりあえずこんな感じにしておくぐらいでしょうか。クールではないですが、ひとまず気持ちは伝わるかと思います…。

  scope :not_blank, ->(column_name) {
    raise ArgumentError, "no such column: #{column_name}" unless column_names.include? column_name
    where "NULLIF(#{column_name}, '') IS NOT NULL"
  }
編集 履歴 (2)

そもそも、なぜ空文字列とNULLを区別して扱いつつ、最終的に検索時にcoerceしてしまうことになるのか、という、前提とされている状況にこそ解決のヒントが隠されているような気がします。

たとえば clear_empty_attributes というgemがあります。 ActiveRecord::Basebefore_validationbefore_saveのフックを設置して、空文字列をみつけるとDBに保存する前時点で""をNULLに置き換えてしまいます。古いライブラリですが、Rails 3.2現在まで有効です。

既存の""をすべてNULLに置き換えるrake taskも含まれていますので、検討されてはいかがでしょうか。

https://github.com/grosser/clear_empty_attributes

ここにも書かれていますが、NULLと""を区別して扱うのは、ほぼ何も嬉しいことがなくてトラブルだけを増やしてくれます。実際、Oracleなど空文字列とNULLを区別しないデータベースさえあります。

インデックスに関しては、MySQLのようにNULLをインデックスに入れるものもあれば、PostgreSQLのように入れないものもあるので、そこが問題になるならば、逆にnilを""で置き換えてからsaveするような挙動にしてしまうのもアリだと思います。

ただでさえNULLの存在でややこしい3値論理を、(その複雑性に見合うだけのメリットが無い限り)空文字列の導入で4値論理にしてしまうことを避ける、というのがポイントになるかと思います。

編集 履歴 (1)

Railsがエスケープしているようにエスケープ(というか良い感じのクォート)したいのであれば、
ActiveRecord::Base.connection.quote_column_name()を使えば良いのではないでしょうか。

ただし、その場合も最終的には「クォートしたものを式展開で埋め込む」ことになりますので、相応の注意が必要というか、クールさにかけますね。

このあたりのコードは、 https://github.com/rails/rails/blob/master/activerecord/lib/active_record/sanitization.rb にいろいろと載っていますので、アプリの機能に良い感じに援用できるメソッドを探してはどうでしょうか。

編集 履歴 (0)
ウォッチ

この質問への回答やコメントをメールでお知らせします。