覚えておくと幸せになれるActiveRecord::Relationメソッド6選


参照元はMitch Crowe氏のこちらのエントリー。時期にして3年以上前の更新になりますが、未だに参照することが多いものをこの際敬意を込めてまとめることにしました。

1. first_or_create(ブロック付き)

任意のレコードを新規保存する際に、既に同じ情報を持ったレコードがある場合被らせたくない!というニーズにクリティカルに答えてくれるのがfirst_or_createメソッド。whereメソッドと併用することで、検索条件に合致するレコードが存在する場合にはそのレコードを参照し、無ければ検索条件の内容で新しいレコードを新規保存してくれます。返り値は参照したレコードもしくは新規作成したレコードです。

百聞は一見に如かず
Book.where(title: 'Frozen').first_or_create

# Book Load (1.7ms) SELECT `books`.* FROM `books` WHERE `books`.`title` = 'Frozen' ORDER BY `books`.`id` ASC LIMIT 1

## 該当するデータが存在しない場合のみ検索条件の内容で新規レコードを作成
#   (0.1ms) BEGIN
# SQL(29.9ms) INSERT INTO `books` (`created_at`, `title`, `updated_at`) VALUES ('2015-06-26 15:58:59', 'Frozen', '2015-06-26 15:58:59')
#   (1.9ms) COMMIT

しかしレコードの検索をかけたタイミングで別のプロパティ値を設定したくなる時が頻繁に出てくる。例えば既存のレコードを参照するにしろ新規レコードを作成するにしろ、新たに著者(author)の情報も追加して保存したい、といったケースです。first_or_initializeメソッドとsaveメソッドを併用すれば解決しますが、なんとふたつもメソッド使わなくていいんです。 first_or_createメソッドはブロックを引数に持つことができます

百聞は一見に如かず
Book.where(title: 'Frozen').first_or_create do |book|
    book.author = 'Charles Dickens'
end

# Book Load (1.7ms) SELECT `books`.* FROM `books` WHERE `books`.`title` = 'Frozen' ORDER BY `books`.`id` ASC LIMIT 1

## 検索条件ではないauthorのデータも同時に保存できる
#   (0.1ms) BEGIN
# SQL(29.9ms) INSERT INTO `books` (`created_at`, `title`, `author`, `updated_at`) VALUES ('2015-06-26 15:58:59', 'Frozen', 'Charles Dickens', '2015-06-26 15:58:59')
#   (1.9ms) COMMIT

2. none

Rails4からサポートされた機能。名前から想像できるとおり空のモデルオブジェクトを生成して返り値とします。例えばユーザーのコンディションによっては、何をやっても「該当するデータは存在しませんでした」というメッセージだけを返すなど違和感なく機能を制限したいときに使えるメソッドです。

百聞は一見に如かず
@posts = current_user.available_posts

## 有効期限切れのユーザーに対してはレコードが何も無いように見せたい時
def available_posts
  case role
  when 'Admin'
    Post.all
  when 'Reviewer'
    Post.where(condition: published)
  when 'Expired'
    Post.none
  end
end

3. find_each

ぼくらのイテレータeach。しかし数千〜数万のレコードに対して同一の処理を繰り返したいときに使用すると、単一のクエリがレコードの数分発行されてしまうのでアプリが素敵にフリーズします。そんな時はfind_eachメソッドがよさげな様子。1000件のデータをまとめてバッチ処理してくれるために処理が高速化します。

百聞は一見に如かず
Book.where(author: nil).find_each do |book|
  book.author = "not to be filled yet"
  book.save
end

4. find_by

テーブルから条件に該当するただひとつのレコードを取得したいときBook.where(title: 'Frozen', author: 'Charles Dickens').firstとするのはなんだかスマートじゃないですよね。その時に使えるのがfind_byメソッドです。条件に合致するただひとつのレコードを取得し配列に入れずに返してくれます。

百聞は一見に如かず
Book.find_by(title: 'Frozen', author: 'Charles Dickens')

## 返り値は配列に入っていない
# Book Load (0.3ms)  SELECT `books`.* FROM `books` WHERE `books`.`title` = 'frozen' AND `books`.`author` = 'Charles Dickens' LIMIT 1
# => #<Book id: 1, title: "Frozen", author: "Charles Dickens", created_at: "2015-06-27 03:11:23", updated_at: "2015-06-27 03:11:23">

5. pluck

任意のカラムの値で配列化したい時、mapメソッドを使って以下のようにやっていませんか?

百聞は一見に如かず
@book_titles = Book.all.map(&:title)

代わりにpluckメソッドを使ってあげましょう。
all × mapメソッドの組み合わせと比較してみると、実行速度の面からも著しい改善が見られるようです

百聞は一見に如かず
@book_titles = Book.pluck(:title)

# => ["Frozen", "Tower", "the Lion King", ...]

6. merge

2つのハッシュを結合し、キーとバリューを維持したまま1つのハッシュにして返り値としてくれます。なお同じキーの値があった際には、mergeメソッドの引数として渡した値の方が優先される模様。merge!という破壊的メソッドも用意されていて、こちらはレシーバ自身の値を書き換えてしまいます。レコードを保存する際に、複数のハッシュを単一のハッシュにできるmergeは大変重宝します。

百聞は一見に如かず
book_data = {title: "Frozen", author: "Charles Dickens"}
book_review = {rate: 10, review: "Awesome!"}

book_data.merge(book_review)

# => {title: "Frozen", author: "Charles Dickens", rate: 10, review: "Awesome!"}

以上となります。

オリジナルの英語記事では、使用頻度の関係から紹介できなかったscopingなどについても解説されていますので、ぜひ一度ご覧になってみてください。