Rails で十分に活用されていなくてもったいない ActiveRecord::Relation のメソッド TOP 10

2013年12月2日更新: 参照されることが多いので Rails 4 の情報を訳注として追記しました。また、Rails 4 に関する情報は、 WEB+DB PRESS Vol.73 が非常に参考になるので、一読をおすすめします。

この文章は Mitch Crowe 氏のブログより 2012年4月14日の記事を翻訳したものです。


The 10 Most Underused ActiveRecord::Relation Methods
http://blog.mitchcrowe.com/blog/2012/04/14/10-most-underused-activerecord-relation-methods/


昨日は ActiveRecord::Relation のコードに膝まで浸かって、使われているのをこれまで全然見たことがない面白いナゲットを思い出させてくれた。この記事で、十分に活用されていない Relation クラスのメソッドのトップ10をリストにしたので、楽しんでもらいたい。

10位 ブロック付きの first_or_create

first_or_create はとてもなじみ深い。

Book.where(:title => 'Tale of Two Cities').first_or_create

そして、名前通りのことをやってくれる。だが、特定の属性を持つレコードを find するとか、それらの属性を持つレコードを create して、さらに追加で属性を設定したくなることが頻繁にあると思う。これを簡潔にやるには first_or_create にブロックを与えればいい。

Book.where(:title => 'Tale of Two Cities').first_or_create do |book|
  book.author = 'Charles Dickens'
  book.published_year = 1859
end

9位 first_or_initialize

このレコードをまだ保存したくない場合は、 first_or_initialize が使える。

Book.where(:title => 'Tale of Two Cities').first_or_initialize

8位 scoped

あるクラスの持つすべてのレコードを表した ActiveRecord::Relation がほしいことがある。そんなときは scoped メソッドを使えば簡単に生成できる。(訳注: Rails 4 では、 scoped が非推奨になり、 all が Array から ActiveRecord::Relation のオブジェクトを返すようになりました。Rails 4 では scoped から all に変更してください。)

def search(query)
  if query.blank?
    scoped
  else
    q = "%#{query}%"
    where("title like ? or author like ?", q, q)
  end
end

7位 none ( Rails 4 のみ)

同じように、オブジェクトを含まない ActiveRecord::Relation がほしいことがある。空の Array を返すことが、大抵それほどよくないのは、 API の利用者が Relation オブジェクトを期待しているからだ。代わりに none を使えばいい。

def filter(filter_name)
  case filter_name
  when :all
    scoped
  when :published
    where(:published => true)
  when :unpublished
    where(:published => false)
  else
    none
  end
end

注意:最先端を行く人は今すぐ none を使いたくなっていることだと思う。これは、 Rails 3 ではなく、 Rails 4 で利用できる。だが、Rails 4 を待つまでの間も簡単に書くことができる。 この Stack Overflow のスレッドをチェックしてほしい。

6位 find_each

数千レコードをイレテートさせたくなっ場合、each を使いたくはないだろう。
each は1 つのクエリを実行して、全レコードを取得し、それらすべてをメモリ上にインスタンス化してしまう。メモリを十分に確保しているならいい。確保してなければ、これは Rails アプリをフリーズさせる素敵な方法になる。find_each は、そうではなく、レコードをバッチ処理で find して(デフォルトは1000件)、一度に yield する。その結果、同時に全レコード分のメモリを確保する必要がなくなる。

find_each は yield されるレコードの順序を指定できないので注意だ。指定してもただ単に無視される。

Book.where(:published => true).find_each do |book|
  puts "Do something with #{book.title} here!"
end

5位 to_sql と explain

ActiveRecord は素晴らしいが、思った通りのクエリをいつも生成してくれるとは限らない。コンソールに飛んで、組み立てた Relation で この 2 つの命令を実行してみよう。賢いクエリにマップされているか確認して、そうなってなかったら愛情込めて作ったインデックスを使うようにしよう。

Library.joins(:book).to_sql
# => SQL query for you database.
Libray.joins(:book).explain
# => Database explain for the query.

4位 find_by(Rails 4 のみ)

Rails で書いたコードは次のような行で散らかったものになりやすい。

Book.where(:title => 'Three Day Road', :author => 'Joseph Boyden').first

代わりに、 find_by というショートカットメソッドが使える。

Book.find_by(:title => 'Three Day Road', :author => 'Joseph Boyden')

これは上と同じものが実行される。

注意:最先端を行く人は今すぐ find_by を使いたくなっていることだと思う。これは Rails 3 ではなく、 Rails 4 で利用できる。

3位 scoping

scope メソッドを特定の Relation として使える。Rails のドキュメントにある次の例を検討してみよう。

Comment.where(:post_id => 1).scoping do
  Comment.first # SELECT * FROM comments WHERE post_id = 1
end

これはまったくもって使いやすい。

2位 pluck

特定のレコードのカラムで配列にしたいことがないだろうか?私はこういうにを本当にたくさん見てきた。

published_book_titles = Book.published.select(:title).map(&:title)

さらに悪い場合だとこうだ。

published_book_titles = Book.published.map(&:title)

代わりに pluck を使おう。

published_book_titles = Book.published.pluck(:title)

(訳注: Rails 3 では、pluck の引数に指定できるシンボルは 1 つでしたが、 Rails 4 ではシンボルの配列を指定することで、属性を複数指定することができます。select で複数の属性を指定した場合と違い、2次元配列を返します。3 系でも比較的最近のバージョンで使えるはずです。)

1位 merge

僕はこの宝石なしには生きられない。しかし奇妙なことに、これはソース中にドキュメントされていないし、今まで見たガイドでも言及がない。これを使うと、結合( JOIN )ができて、結合されたモデルに対して名前付きスコープでフィルタできる。(訳注: 私見ですが、4.0.0 では merge が意図と違う動作をしないかよく確認した方がよいと思います。*1 いくつか修正が入っている4.0.1 以降でも試してみてください。)

class Account < ActiveRecord::Base
  # ...

  # Returns all the accounts that have unread messages.
  def self.with_unread_messages
    joins(:messages).merge( Message.unread )
  end
end

WEB+DB PRESS Vol.73

WEB+DB PRESS Vol.73