LoginSignup
90
65

More than 5 years have passed since last update.

そこはhas_oneを使いましょうよ

Last updated at Posted at 2017-05-13

はじめに

has_manyなどで関連付けされているモデルで特定の条件の関連レコードを取得する際に安易にメソッドを定義したりしていませんか?

たとえば

# ユーザー
class User < ApplicationRecord
  has_many :articles
end

# 記事
class Article < ApplicationRecord
  belongs_to :user
end

という関係があった場合、最新の記事を取得するのに

def latest_article
  articles.order(created_at: :desc).first
end

などというメソッドを安易に定義しがちではないかと思います。

メソッドの場合

これが一つのviewで一度しか呼び出されないのならば特に問題はないかと思います。
でもリスト表示などの場合、

# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def index
    @users = User.all
  end
end
# index.html.erb
<% @users.each do |user| %>
  <%= user.name %>
  <%= user.latest_article.title %>
<% end %>

のようなviewを表示するのに

  User Load (0.5ms)  SELECT `users`.* FROM `users`
  Article Load (0.4ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`created_at` DESC LIMIT 1
  Article Load (0.4ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 2 ORDER BY `articles`.`created_at` DESC LIMIT 1
  Article Load (0.3ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 3 ORDER BY `articles`.`created_at` DESC LIMIT 1

とユーザーの数だけArticleをロードするようになります。
さらに

# index.html.erb
<% @users.each do |user| %>
  <%= user.name %>
  <%= user.latest_article.title %>
  <%= user.latest_article.body %>
<% end %>

などのようにしようものなら、

  User Load (0.3ms)  SELECT `users`.* FROM `users`
  Article Load (0.4ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`created_at` DESC LIMIT 1
  CACHE Article Load (0.0ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`created_at` DESC LIMIT 1  [["user_id", 1], ["LIMIT", 1]]
  Article Load (0.3ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 2 ORDER BY `articles`.`created_at` DESC LIMIT 1
  CACHE Article Load (0.0ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 2 ORDER BY `articles`.`created_at` DESC LIMIT 1  [["user_id", 2], ["LIMIT", 1]]
  Article Load (0.5ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 3 ORDER BY `articles`.`created_at` DESC LIMIT 1
  CACHE Article Load (0.0ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 3 ORDER BY `articles`.`created_at` DESC LIMIT 1  

キャッシュを使っていますがuser.latest_articleがメモ化されるわけでもなく、その都度Articleを探しに行ってしまいます。

has_oneを利用した場合

そこでメソッドでなく以下のように関連付けで定義してみると

  has_one :latest_article, lambda {
    order(created_at: :desc)
  }, class_name: :Article
# index.html.erb
<% @users.each do |user| %>
  <%= user.name %>
  <%= user.latest_article.title %>
  <%= user.latest_article.body %>
<% end %>
  User Load (0.4ms)  SELECT `users`.* FROM `users`
  Article Load (0.3ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 1 ORDER BY `articles`.`created_at` DESC LIMIT 1
  Article Load (0.4ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 2 ORDER BY `articles`.`created_at` DESC LIMIT 1
  Article Load (0.3ms)  SELECT  `articles`.* FROM `articles` WHERE `articles`.`user_id` = 3 ORDER BY `articles`.`created_at` DESC LIMIT 1

view内では同じく二度latest_articleが呼ばれているのですが、ArticleをLoadするのは一度づつに減っています。

さらにコントローラーでpreloadしてやると

class HomeController < ApplicationController
  def index
    @users = User.preload(:latest_article).all
  end
end
  User Load (2.1ms)  SELECT `users`.* FROM `users`
  Article Load (0.3ms)  SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN (1, 2, 3) ORDER BY `articles`.`created_at` DESC

とArticleをLoadするのは一度だけになりました。

結論

has_manyなどで既に関連付けされているモデルから特定の条件でレコードを取得する際に安易にメソッドやviewから関連モデルを呼び出しがちですが、条件を付加したhas_oneやhas_manyで定義することでパフォーマンスにもお財布にも優しいコードになるかと思います。

90
65
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
90
65