2012年12月3日月曜日

Arel scope の OR

http://dodemoyoiblog.blogspot.jp/2012/09/scope.html
上の記事でも書いたが、Arel の scope は便利だ。これは何度言っても言い足りない。

細かいことは元記事を読んでもらうとして、
scope にビジネスロジック上の重要概念を表現しておいて、クエリでは scope の組み合わせを用いることで、複雑なクエリでも極めて簡潔に表現することができる。

scope にビジネスロジックの概念を表現させて組み合わせることによって得られるメリットは、(1)個々の概念に明確に名前が付けられるため開発者間でのコミュニケーションがスムーズになる。DDD のユニバーサルランゲージと役割が近い。(2)ビジネスロジックという比較的変化しにくくアプリ全体で利用されるものをパックしているので再利用性が高い。(3)クエリ側の修正も scope 単位での追加削除によってなされることになるので、sql や AREL の where/join などを直書きするよりも間違いが起きにくい。where や join の直書きでは、複数の join, where などの組み合わせでひとつの役割を果たすことが多いがそれは字面を読んでもわからない。(4)ビジネスロジックそのものに修正が入った場合も、そのビジネスロジックがひとつの scope の中に閉じ込められて表現されていることが多いため、変更がスムーズ。

などだ。

そしてさらに嬉しいのが scope の merge だ。これは複数の scope をくっつけることができる。例えば


User.valid.merge( User.has_email )

のように書くと

User.valid.has_email

と同じ意味になる。これだとあまり旨みがわからないかもしれないが、merge の効果が高まるのは両者のクラスが異なるときだ。

User.valid.join(:emails).merge( Email.email_contains( str ) )
Admin.valid.join(:emails).merge( Email.email_contains( str ) )

このように、Email の scope を別のモデルのクエリに使いまわすことができる。
上の例のような単に like で検索するみたいな scope だと正直あまり意味はないが、複雑なビジネスロジックが scope で表現されているときこれは非常にありがたい。


あたりが参考になる。

しかし、残念なことがひとつある。merge は and が表現できるが、or が表現できない。ドキュメントには記載されているらしいが、使えない。

例えば

User.valid.join(:email, :tels).merge( Email.email_contains(email).or( Tel.tel_contains( tel )) )

みたいなことがしたい。だがまだ未実装である。

そこで、限定的ではあるが、以下のような書き方をすることで scope を再利用しつつ OR が使える。

User.valid.join(:email, :tels).merge( 
  "(#{Email.email_contains(email).where_values.join(' AND ')}) OR (#{Tel.tel_contains( tel ).where_values.join(' AND ')})"
)

え、きたならしい?そこは適当にメソッドを作って、例えば以下のコードで上と同じ動きをするようにすればよい。
User.valid.join(:email, :tels).merge( 
  are_or( 
    Email.email_contains(email),
    Tel.tel_contains( tel )
   )
 )

大事なことは概念が scope としてパックされてアプリ全体で使いまわされるということだ。

以上。