Squeelの列名を動的に指定する


Squeelを使うと、Rubyの枠内で複雑なクエリもすいすい書けるようになりますが、さらに凝ったことをしようとするとひと工夫が必要になります。

Squeelって?

Railsでちょっと複雑なSQLのクエリを書こうとすれば、一部を文字列にするか、Arelを使うかということになりますが、どちらにも使いづらい点があります。

  • 文字列で書いてしまうと、あとあとの再利用が不便になる
  • Arelは内部向けのAPIであって、使いにくい&仕様が安定していない

ここで登場するのがSqueelです。SqueelはArelをラップして、Ruby的なDSLでSQLを書けるようにしてくれます。使い方については他のQiita記事タイムインターメディアさんの記事に詳しいです。

動作を考える

たとえば、「身長が160cm以上、体重が70kg以下」というような条件で検索をかけるとなると、こんな感じになります。

DSL
Person.where{(height >= 160) & (weight <= 70)}

さて、heightweightなんて定義した覚えはないのですが、いったいどうなっているのでしょうか。whereの引数がブロックとなっていることからも予想できるかもしれませんが、じつはこのブロック全体がinstance_evalで評価されます。そして、存在しない名前についてはmethod_missingが拾っていって、評価式を組み立てるためのオブジェクトに変換してしまう、という仕組みになっています。

動的に指定…できる?

さて、ちょっと凝ったことをしたくなったので、Squeelで条件とする列名をハードコードするのではなく、シンボルなどで外から与える必要が出てきました。ただ、呼ぶべきものは明示的なメソッドがあるわけではない、method_missing上の機能なので、一体どうすればいいのでしょうか。

と思って調べてみると、sendでメソッドを呼んだ場合にも、当該メソッドがなければmethod_missingに回ることが判明しました。ということで、DSL内部でsendを使ってみることにしました。

失敗例
column = :height
Person.where{send(column) >= 160}

とりあえず実行時にエラーは起きなかったのですが、これでDBを参照してみるとSQL段階でエラーとなってしまいました。原因を調べるために.to_sqlとしてみると、SELECT (中略) WHERE send(height) >= 160というように、sendがDB関数だと解釈されてしまっていました。

非常用メソッド

ということで、この環境ではsendすら削除されてmethod_missingに流れるようになってしまっていました。ただし、sendにはもう1つの名前である__send__というのがあって、これはBasicObjectにすら用意されているものです。こちらでしてみると、どうなるでしょうか。

成功例
column = :height
Person.where{__send__(column) >= 160}

こうすることで、きちんと本来のメソッドまでコードが回るようになりました。