LoginSignup
14
13

More than 5 years have passed since last update.

Scope作成時に気をつけたいこと + Solidusにおけるscope追加方法

Last updated at Posted at 2018-12-09

はじめに

本記事は、以下の目次に従って書きました。

環境
Ruby 2.5.1
Rails 5.2.1
例に使用したgem solidus

  1. そもそもscopeとは何か?
  2. solidusにおけるscopeの定義のやり方
  3. 具体例とscope作成時に気をつけたいこと
  4. scope作成時にハマったこと
  5. おわりに

1. そもそもscopeとは何か?

スコープを設定することで、関連オブジェクトやモデルへのメソッド呼び出しとして参照される、よく使用されるクエリを指定することができます。スコープでは、where、joins、includesなど、これまでに登場したすべてのメソッドを使用できます。

参考 RAILS GUIDES

つまり、モデルに対して使用するSQL文をメソッド化できます。よく使いそうなクエリなどを、わかりやすいメソッド名をつけて定義してあげると、凡用性が上がりそうです。

2. solidusにおけるscopeの定義のやり方

  1. 名前に_decoratorとつける
  2. 作成したいdecorator内で、モデル名.class_evalと定義する
  3. 作成したいscopeを定義
product_decorator.rb
Hoge::Product.class_eval do
  scope :stock_products, -> { where(sold: false) }
end

3. 具体例とscope作成時に気をつけたいこと

(scope使用前)

products_controller.rb
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使用後)

products_controller.rb
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
product_decorator.rb
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内でインスタンス変数を使用して依存度を上げている

product_decorator.rb
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. 終わりに

以上、ご一読ありがとうございました。
ご指摘等ございましたらご一報いただけると幸いです。

14
13
0

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
14
13