Rails 4.1 で arel-helpers を使う

  • 10
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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

公式

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

試した環境

% ./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

準備

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

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

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

Gemfile
gem 'arel-helpers'

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

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

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

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

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 起動。

% ./bin/rails c

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

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

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

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

これで準備は完了。

使ってみる

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

> Post.where(Post[:id].eq(1))
  Post Load (0.9ms)  SELECT "posts".* FROM "posts"  WHERE "posts"."id" = 1
=> #<ActiveRecord::Relation []>

これで毎回 arel_table と書かなくて良くなるので、OR なんか書く時はかなりすっきりする。

> 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 がシンプルに書ける。

> 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 はいい感じ。

> 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 にブロックを渡して指定する。

> 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 を呼べるようになる。~

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

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

> 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 を継承した以下のようなクラスを作成。

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 上で読み込む。

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

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

> 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 を多用するハメになるので、ちょっとすっきり書けるだけでもかなり有用なライブラリだと思う。

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