はじめに
rails単体でのアプリケーションでは正直scopeというものは使わなかったですが、実務においてSPAだとかなり使う面があったので、今回記事にしました。
scopeとは何か
Rails ガイドさん より
よく使うクエリをスコープに設定すると、関連オブジェクトやモデルへのメソッド呼び出しとして参照できるようになります。スコープでは、where、joins、includesなど、これまでに登場したメソッドをすべて使えます。どのスコープメソッドも、常にActiveRecord::Relationオブジェクトを返します。スコープの本体では、別のスコープなどのメソッドをスコープ上で呼び出せるようにするため、ActiveRecord::Relationかnilのいずれかを返すようにすべきです。
簡単にいうとSQL文をモデルに対してメソッド化して使える仕組みのことです。
なぜscopeを使うべきか
私の業務でもSPAでの開発を行っているのですが、scopeを使う第一メリットとしてscopeで必要なデータのみの取得を行うためquery(クエリ)の最適化ができSPAのパフォーマンス向上につながる。ことやSPAでデータのフィルタリングができるため用いられる。 ことが私の第一印象です。
他にも
Pikawaka さんより
- 条件式に名前を付けられるので、直感的なコードになる
- 修正箇所を限定することが出来る
- コードが短くなる
というメリットがあります。
scopeの基本
基本的な構文
class モデル名 < ApplicationRecord
scope :スコープの名前, -> { 条件式 }
end
scopeの名前は自分でつけることができます(任意の名前ok)
基本的な構文は上の形になるのですが、じゃあどんな時に使うのかとなると
class HomeController < ApplicationController
def index
if params[:kind] == 'following'
@posts = Post.includes(:user).where(user_id:
[*current_user.followings.ids]).order('created_at DESC').page(params[:page]).per(9)
else
@posts = Post.includes(:user).order('created_at DESC').page(params[:page]).per(9)
@post = Post.new
end
end
これは私が未経験の頃rails単体でアプリケーション作成中のコードですね。
何してるかというと、簡単なフォロー関係にある投稿を最新順に9投稿取得するというコードになっています。(gemのkaminariを使用)
見てくれたらわかると思うのですが、やたら冗長ですね。
このSQL文の部分をPost モデルにscopeとして記述すると
class Post < ApplicationRecord
belongs_to :user
scope :with_users, -> { includes(:user) }
scope :newest_first, -> { order('created_at DESC') }
scope :from_followed_users, -> (user) { where(user_id: user.followings.ids) }
scope :paginate, ->(page, per_page = 9) { page(page).per(per_page) }
end
そして、先ほどのControllerに適用させると
class HomeController < ApplicationController
def index
@posts = Post.with_users.newest_first
if params[:kind] == 'following'
@posts = @posts.from_followed_users(current_user)
else
@post = Post.new
end
@posts = @posts.paginate(params[:page])
end
今ちょっと修正しましたけど、あ〜ら不思議スッキリしたコードになりました!
このようにして、Controllerで使い回すSQL文の部分はspecに書くとDRYになります!
ちなみにscope paginateの第二引数はデフォルト値であって
Post.paginate(3,15)
# 3ページ、15個のdataを取得
ともできます。
さらに
scopeの ' -> 'はラムダ式の省略記法を表しています。つまり
scope :from_followed_users, lambda { |user|
where(user_id: user.followings.ids)
}
scope :paginate, lambda { |page, per_page = 9|
page(page).per(per_page)
}
こっちでも良きです。
さらなる使用例
UserとPostに(1対多)、PostとCommentに(1対多)の関係があるとします。
あるUserからそのUserに対するCommentを取得するとします。すると、Postの数×Commentの数だけ試行回数を行う必要があります。そこでUserとPostをjoinしてUserのidで絞るとCommentの数以下の試行回数ですみます。
ここで、scopeの出番です。
class Comment < ApplicationRecord
has_one :user, through: :post
belongs_to :post
scope :users_with_posts, lambda { |user_id|
join(:user)
.where(user: {user_id:})
.distinct
}
end
ここでこのscopeメソッドを実際に使うためには
has_one :user, through: :post
belongs_to :post
has_many :comments
belongs_to :user
このリレーションシップを記述していないとscopeはうまくいきませんでした。
また、今回は親、子、孫の関係でhas_one :user, through: :post
があるのでjoinの文はjoin(:user)
で良かったのですが、Postとfavorite(いいね機能),PostとCommentのような場合だとjoin(post: :favorites)
と記述する必要があります。
このようにすることでSPAだとクライアントサイドの要求に対して効率的なAPIレスポンスを提供することができ,SPAのパフォーマンス向上につながると考えられます。
scopeとクラスメソッドの違い
いろいろ読ませていただきました。大変勉強になりました。
- scopeはメソッドチェーンができるというところ
- 引数にnilを取るとscopeはall, クラスメソッドはfalseが返り値となる
それ以外は大きな違いはないということのようです。
ですが、この記事を拝見してから
ActiveRecord::Relation オブジェクトを返す時はscopeを使う!
終わりに
scopeを使うことでコードの可読性や再利用性の向上があるので、railsを使う初心者はぜひ学習してみてはいかがでしょうか。
私も早く『私はITエンジニアです』と名乗れるようになりたいですねぇ(5年後くらいかな)
誤り、改善等ございましたらご指摘していただくとありがたいです。
ここまでありがとうございました。