ArelでLIKEとかいうRails

同じ名前のカラムがあるモデルに関連があるとする。FooとBarがあってそのどちらにもname属性があるようなケース。

Foo.where(:name => 'baz')

これはもちろん問題なく実行できる。次のような関連を使う場合でも特に問題は起きない。

Foo.joins(:bar).where(:name => 'baz')

ここでハッシュ表現できないような条件を考えてみる。

Foo.where('name like "baz"')

これももちろん問題ないのだけれど、次のようなのになるとちょっと困ってしまう。

Foo.joins(:bar).where('name like "baz"')

もしもこれを実行しようとするとStatementInvalid例外が上がってくる。SQL文を生成させてみると「WHERE (name like "baz")」となってしまっているのを確認できる。

whereメソッドで指定するSQL片の書き方を変えればこの状況を回避できる。ただ、ここでテーブル名を持ち出すというのには引っかかりあって、これはどうにかならないものかと思ってしまう。table_nameメソッドを使うなどしてテーブル名を書かないようにしてみたりもするが、それも少し引っかかる。

ある時、Rails 3では次のように書けるというのを知った。

t = Foo.arel_table
Foo.joins(:bar).where(t[:name].matches('baz'))

こちらで生成されるSQL文では「WHERE (foos.name LIKE 'baz')」のようになり、上のような例外は発生しない。

これがArel。Arelを使うと、この他にも、たとえば「or」なんかもSQL直書きをせずにすませられる。次のようにかなり強力である。

Foo.where(t[:name].eq('foo').or(t[:name].eq('bar'))).order(t[:name].asc))
#=> ... WHERE ((`foos`.`name` = 'foo' OR `foos`.`name` = 'bar')) ORDER BY `foos`.`name` ASC

実際、Arelは新しくなったActiveRecordを支えているものの一つであって、Rails 3でおなじみになった次のような書き方もこのArelのレイヤで可能となっている。

c1 = t[:name].eq('foo')
c2 = t[:name].eq('bar')
Foo.where(c1.or c2)

もっともこれらは例のための例といったものでもあって、このような単純なケースであれば次のように書いてしまったほうが早いかもしれない。

Foo.where(t[:name].eq_any %w(foo bar))

詳しくはRuby Freaks Lounge第43回 Rails 3を支える名脇役たち その1 - Arel -を。どんなメソッドがあるのかについては今のところソースコードを見るのかな。(ところでmatchesで生成されるLIKEへのESCAPEを指定する方法はあるのだろうか。)