Rails 4.1 で arel-helpers を使う


ActiveRecord で arel_table を使い易くするライブラリ arel-helpers の README.md のサンプルを試しに動かしてみた。

## 公式

camertron/arel-helpers
https://github.com/camertron/arel-helpers


## 試した環境

```shell-session
% ./bin/rake about
About your application's environment
Ruby version              2.1.3-p242 (x86_64-linux)
RubyGems version          2.2.2
Rack version              1.5
Rails version             4.1.8
JavaScript Runtime        Node.js (V8)
Active Record version     4.1.8
Action Pack version       4.1.8
Action View version       4.1.8
Action Mailer version     4.1.8
Active Support version    4.1.8
```

## 準備

適当なプロジェクトを作成。

```shell-session
% rails new ahexample --skip-bundle && cd ahexample && bundle install --path=vendor/bundle
```

Gemfile に以下を追記して再度 bundle install。

```ruby:Gemfile
gem 'arel-helpers'
```

arel-helpers 2.0.1 がインストールされた。

サンプルのテーブル構造はライブラリ本体の spec から拝借した。
arel-helpers の spec に含まれるテストテーブル作成用の migrations.rb とテスト用モデルクラスが定義された models.rb を lib 以下にコピー。

```shell-session
% cp vendor/bundle/ruby/2.1.0/gems/arel-helpers-2.0.1/spec/env/* ./lib
```

このままだと README.md のサンプルが上手く動作しないので migrations.rb の方を以下のように修正した。

```ruby:migrations.rb
# encoding: UTF-8

class CreatePostsTable < ActiveRecord::Migration
  def change
    create_table :posts do |t|
      t.column :title, :string
      t.column :created_at, :datetime
      t.references :author
    end
  end
end

class CreateCommentsTable < ActiveRecord::Migration
  def change
    create_table :comments do |t|
      t.references :post
      t.references :author
    end
  end
end

class CreateAuthorsTable < ActiveRecord::Migration
  def change
    create_table :authors do |t|
      t.references :collab_posts
      t.column :username, :string
    end
  end
end

class CreateFavoritesTable < ActiveRecord::Migration
  def change
    create_table :favorites do |t|
      t.references :post
    end
  end
end

class CreateCollabPostsTable < ActiveRecord::Migration
  def change
    create_table :collab_posts do |t|
      t.references :authors
    end
  end
end
```

rails console 起動。

```shell-session
% ./bin/rails c
```

コピーしたファイルを読み込む。

```ruby
> require_relative './lib/models'
=> true
> require_relative './lib/migrations'
=> true
```

以下を実行してテーブル作成。

```ruby
> CreatePostsTable.new.change
> CreateCommentsTable.new.change
> CreateAuthorsTable.new.change
> CreateFavoritesTable.new.change
> CreateCollabPostsTable.new.change
```

これで準備は完了。

## 使ってみる

arel-helpers を使うと `Post.arel_table[:id]` のような記述が `Post[:id]` と書けるようになる。

```ruby
> Post.where(Post[:id].eq(1))
  Post Load (0.9ms)  SELECT "posts".* FROM "posts"  WHERE "posts"."id" = 1
=> #<ActiveRecord::Relation []>
```
これで毎回 `arel_table` と書かなくて良くなるので、OR なんか書く時はかなりすっきりする。


```ruby
> Post.where(Post[:id].eq(1).or(Post[:id].eq(2)))
  Post Load (0.3ms)  SELECT "posts".* FROM "posts"  WHERE (("posts"."id" = 1 OR "posts"."id" = 2))
=> #<ActiveRecord::Relation []>
```

`ArelHelpers.join_association` を使う事で、ActiveRecord + arel_table だと複雑になってしまうような JOIN がシンプルに書ける。

```ruby
> Post.joins(ArelHelpers.join_association(Post, :comments))
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
=> #<ActiveRecord::Relation []>
```

単純な INNER JOIN なら普通に ActiveRecord で JOIN した方が良さそう。
LEFT OUTER JOIN はいい感じ。

```ruby
> Post.joins(ArelHelpers.join_association(Post, :comments, Arel::OuterJoin))
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
=> #<ActiveRecord::Relation []>
```

JOIN に複雑な条件を指定したい場合は `join_association` にブロックを渡して指定する。

```ruby
> Post.joins(
*   ArelHelpers.join_association(Post, :comments, Arel::OuterJoin) do |assoc_name, join_conditions|
*     join_conditions.and(Post[:author_id].eq(4))
>   end
> )
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" AND "posts"."author_id" = 4
=> #<ActiveRecord::Relation []>
```

いちいち `ArelHelpers.join_association` って書くのが面倒な場合は、Model 内で以下のように `ArelHelpers::JoinAssociation` を include する事で直接 Model のメソッドとして `join_association` を呼べるようになる。~

```ruby
class Post < ActiveRecord::Base
  include ArelHelpers::ArelTable
  include ArelHelpers::JoinAssociation
  has_many :comments
  has_many :favorites
end
```

これで先ほどのクエリは以下のように書けるようになる。

```ruby
> Post.joins(
*     Post.join_association(:comments, Arel::OuterJoin) do |assoc_name, join_conditions|
*        join_conditions.and(Post[:author_id].eq(4))
>     end
> )
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" AND "posts"."author_id" = 4
=> #<ActiveRecord::Relation []>
```

検索画面で使う検索用クエリなどのように、より細かく複雑な条件を指定したクエリを組み立てたい場合のために `ArelHelpers::QueryBuilder` というクラスが提供されている。
lib 以下に `ArelHelpers::QueryBuilder` を継承した以下のようなクラスを作成。

```ruby:lib/post_query_builder.rb
class PostQueryBuilder < ArelHelpers::QueryBuilder
  def initialize(query = nil)
    # whatever you want your initial query to be
    super(query || post.unscoped)
  end

  def with_title_matching(title)
    reflect(
      query.where(post[:title].matches("%#{title}%"))
    )
  end

  def with_comments_by(usernames)
    reflect(
      query
        .joins(:comments => :author)
        .where(author[:username].in(usernames))
    )
  end

  def since_yesterday
    reflect(
      query.where(post[:created_at].gteq(Date.yesterday))
    )
  end

  private

  def author
    Author
  end

  def post
    Post
  end
end
```

作成したら rails console 上で読み込む。

```ruby
> require_relative "./lib/post_query_builder"
=> true
```

以下のような感じで使う。

```ruby
> PostQueryBuilder.new.with_comments_by(['camertron', 'catwithtail']).with_title_matching("arel rocks").since_yesterday
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" INNER JOIN "authors" ON "authors"."id" = "comments"."author_id" WHERE "authors"."username" IN ('camertron', 'catwithtail') AND ("posts"."title" LIKE '%arel rocks%') AND ("posts"."created_at" >= '2014-12-03')
=> #<PostQueryBuilder:0x007f7d74790088 @query=#<ActiveRecord::Relation []>>
```

`ArelHelpers::QueryBuilder` を継承したクラスを作成する事で複雑なクエリの組み立てもシンプルに記述できるようになる。

実際の案件などで複雑なクエリを `find_by_sql` を使わずに書こうとするとかなり `arel_table` を多用するハメになるので、ちょっとすっきり書けるだけでもかなり有用なライブラリだと思う。

シンプルで覚える事も少ないので、導入してもそれほど混乱を招くことも無さそうなのもいい。