はじめに
本記事は、以下の目次に従って書きました。
環境
Ruby 2.5.1
Rails 5.2.1
例に使用したgem solidus
- そもそもscopeとは何か?
- solidusにおけるscopeの定義のやり方
- 具体例とscope作成時に気をつけたいこと
- scope作成時にハマったこと
- おわりに
1. そもそもscopeとは何か?
スコープを設定することで、関連オブジェクトやモデルへのメソッド呼び出しとして参照される、よく使用されるクエリを指定することができます。スコープでは、where、joins、includesなど、これまでに登場したすべてのメソッドを使用できます。
参考 RAILS GUIDES
つまり、モデルに対して使用するSQL文をメソッド化できます。よく使いそうなクエリなどを、わかりやすいメソッド名をつけて定義してあげると、凡用性が上がりそうです。
2. solidusにおけるscopeの定義のやり方
- 名前に
_decorator
とつける - 作成したいdecorator内で、
モデル名.class_eval
と定義する - 作成したいscopeを定義
Hoge::Product.class_eval do
scope :stock_products, -> { where(sold: false) }
end
3. 具体例とscope作成時に気をつけたいこと
(scope使用前)
class Hoge::ProductsController < ApplicationController
def show
@product = Hoge::Product.find(params[:id])
@latest_products = Hoge::Product.where(sold: false).
order(created_at: :desc).limit(10).reject { |product| product == @product }
end
end
改善すべき点
・可読性があまりよくない。クエリのメソッドが連結していて、わかりにくい。
・再現性が低い。似た処理を別の場所で実装したい時、同様の処理を書かなければならない。
・reject
(Rubyのメソッド)より、SQLレベルで処理した方がパフォーマンスが良くなる。また、インスタンス変数に依存している。
(scope使用後)
class Hoge::ProductsController < ApplicationController
DISPLAY_LIMIT = 10
def show
@product = Hoge::Product.find(params[:id])
@latest_products = Hoge::Product.stock_products.latest(DISPLAY_LIMIT).
without_self(@product)
end
end
Hoge::Product.class_eval do
scope :stock_products, -> { where(sold: false) }
scope :latest, ->(number) { order(created_at: :desc).limit(number) }
scope :without_self, ->(product) { where.not(id: product.id) }
end
改善した点
それぞれの処理を細かく分けて、わかりやすい名前をつけたscopeを作成し、:latest, ->(number)
のように引数も渡せるように処理しています。
また、DISPLAY_LIMIT = 10
とコントローラー側で定義することによって、何の数字かわかりやすいようにしてあります。
こうすることによって、コントローラー側での可読性が上がりますし、他の処理にも使いまわせそうです。
4. scope作成時にハマってしまったこと
自分は下記のような実装をしてしまい、ハマりました。
・scopeが常に何のオブジェクトになるか、を全く考慮していない
・ActiveRecord::Relation
オブジェクトは処理する直前に遅延評価を実行することを知らなかった
・scope内でreject
というRubyのメソッドを使用している
・scope内でインスタンス変数を使用して依存度を上げている
Hoge::Product.class_eval do
scope :products_display, -> { order(created_at: :desc).reject { |product| product == @product } }
end
どのスコープメソッドも、常にActiveRecord::Relationオブジェクトを返します。
reject
はArrayクラス(参考 Rubyリファレンス) なので、この中で使用してしまうとActiveRecord::Relation
ではなくなり、Arrayになってしまいます。
また、Relationオブジェクトは遅延評価するということから、使用するコントローラーの実装内容や、scopeで定義したメソッドを使用する順番によって、エラーが出る可能性があります。実装する順番にかなり依存してしまったり、メソッドチェーンが使用できなかったりします。
5. 終わりに
以上、ご一読ありがとうございました。
ご指摘等ございましたらご一報いただけると幸いです。