【Rails入門】scopeについて解説
はじめに
Railsアプリケーションを作っていく中で、モデルからデータを抽出したり並べ替えたりと行った処理を作成するのは、それほど難しくありません。
しかし、難しくないが故に、同じようなプログラムコードを繰り返してしまっていることはないでしょうか?
Railsの基本理念の1つは、「同じことを繰り返すな (Don’t Repeat Yourself: DRY)」です。つまり、同じようなプログラムコードを書いてしまうことは、Railsプログラマーにとっては悪と言えるものでしょう。
そして、DRYを実現するためにとても有効な機能が、Railsにはあります。それが「scope」です。
「scope」を使えば、何度も同じクエリを書く必要はありません。
ここでは、そんな「scope」について、詳しく解説します。
scopeとは
scopeメソッドは、Railsのスコープ機能を利用するためのメソッドです。
そして、スコープ機能というのは、クエリを定義することができる機能で、何度も同じクエリを使うような場合に、メンテナンス性を上げることができます。
また、定義名を適したものにすれば、可読性も上げることができます。
SQLで並べ替えや絞り込みを行う
例えば、商品名と価格が登録されているItemモデルがあったとします。
そこからデータを取得するときに、価格が1000円未満のものだけを取得した場合、以下のようなプログラムコードを記載することになります。
@item = where(“price < ?”, 1000)
また、価格の降順(高いものから順に)で取得したい場合は、以下のようになるでしょう。
@item = Item.order(“items.price DESC”)
こういったプログラムコードは必要なシーンが複数になることが多いものです。初期の一覧表示だけではなく、編集項目を選択させるようなシーン、表示方法を変更した場合などもあるでしょう。
そのたびに、これらの1文を追記してしまうと、条件が変わってしまった場合などに変更漏れや変更ミスが起こるリスクがあるだけではなく、すべてのシーンでテストを行うための時間も必要になってしまうのです。
そのため、「同じことを繰り返すな (Don’t Repeat Yourself: DRY)」に基づいたプログラムを行わなければいけません。
SQLクエリを定義して使い回す
前述の問題を解決するために、スコープ機能を使ってみましょう。
スコープは、モデルのクラスに定義します。
class Item < ActiveRecord::Base scope :under1000, -> { where(“price < ?”, 1000) } # => 1,000円未満のレコードを取得
scope :desc_list, -> { order(“items.price DESC”) } # => 価格の降順に並んだレコードを取得
end
使用するときは、あたかもモデルのメソッドのように使うことで、該当するレコードを取得できます。
:
@item = Item.under1000 # => 1,000円未満のレコードを取得
:
@item = Item.desc_list # => 価格の降順に並んだレコードを取得
:
もし、条件が変更となった場合は、定義しているモデルを修正するだけで、他のコントローラやViewなど、スコープを利用している部分は変更する必要がありません。そのため、修正漏れのリスクは低く、テストの時間を大きく減らすことができるでしょう。
環境構築
ここから先、具体的に手を動かしながら確認できるようにするため、環境を作成しておきます。
まずは、以下の手順に沿って、環境の作成とデータの登録を行いましょう。
※scaffoldの詳細や、has_manyについては「【Rails入門説明書】scaffoldについて解説」、「【Rails入門説明書】has_manyについて解説」を参照してください
コマンドプロンプトで以下のコマンドを順に実行してください。
[bash]rails new scope_test
cd scope_test
rails generate scaffold Seller name:string
rails generate scaffold Item seller_id:integer title:string price:integer
rake db:migrate
rails c
Seller.create(name:”Okada”)
Seller.create(name:”Tanaka”)
Seller.create(name:”Kimura”)
Item.create(seller_id: 1, title:”book”, price:500)
Item.create(seller_id: 2, title:”computer”, price:200000)
Item.create(seller_id: 2, title:”phone”, price:15000)
Item.create(seller_id: 3, title:”bag”, price:30000)
Item.create(seller_id: 3, title:”watch”, price:15000)
Item.create(seller_id: 1, title:”pen”, price:100)
exit
[/bash]
次に、関連付けを設定します。それぞれのモデルファイルに追記してください。
(app/models/seller.rb)
class Seller < ApplicationRecord
has_many :items
end
(app/models/item.rb)
class Item < ApplicationRecord
belongs_to :seller
end
サーバーを起動し、ブラウザで「localhost:3000/sellers」「localhost:3000/items」にアクセスすると、以下のように表示されます。
https://web-camp.io/magazine/archives/12481
scopeの使い方
ここからは、Railsのスコープ機能について、定義方法や使い方を細かく説明していきます。
ここまでに説明したように、スコープ機能をうまく使うことで、プログラムコードから面倒なクエリ部分を隠し、可読性とメンテナンス性を大きく上げられます。
ぜひ、ここから説明している内容をマスターして、DRYに則ったプログラムを作っていきましょう。
定義方法は2種類
すでに、定義方法は紹介していますが、スコープの定義方法にはscopeメソッドとラムダ式を用いた定義方法と、メソッドを用いた定義方法の2種類があります。
それぞれ、具体的なプログラムコードを紹介しながら説明しましょう。
scopeメソッドとラムダ式を用いた定義方法
すでに紹介した方法が、scopeメソッドとラムダ式を用いた定義方法です。
モデルクラスの中でscopeメソッドに、スコープ名とラムダ式を引数として渡すことで定義します。
構文は、以下です。
scope: スコープ名, ラムダ式
ラムダ式というのは、メソッドをdefで事前に定義するのではなく、特定の処理の中で動的に定義するもので、Javascriptなどで用いられている処理の定義手法です。
ラムダ式の構文は、以下のようになっています。
lambda { |引数| 処理 } # 基本となる定義方法
-> (引数){ 処理 } # 略式の表現(引数あり)
-> { 処理 } # 略式の表現(引数なし)
スコープの場合は、「処理」の部分にクエリとなる処理を記載すれば良いわけです。
メソッドを用いた定義方法
ラムダ式を使うことで、1行で定義できますので、コードはシンプルになります。ただし、ラムダ式に慣れていない人には少し可読性が低いかもしれません。
そんな場合は、メソッドの形式で定義することも可能です。
def self.スコープ名(引数)
処理
end
※引数がなければ、(引数)は省略可能です
先に紹介したラムダ式の形式の定義を、メソッド形式にすると、以下のようになります。
# 1,000円未満のItem
def self.under1000
where(“price < ?”, 1000)
end
# 価格の降順
def self.desc_list
order(“items.price DESC”)
end
試してみよう
では、テスト環境でスコープ機能を使ってみましょう。
まずは、モデルファイルに定義します。Itemモデルにスコープを追加しますので、手を入れるのは「app/models/item.rb」です。
なお、今回は練習ですので、ラムダ式での定義とメソッドの定義の両方を追加します。
(app/models/item.rb)
class Item < ApplicationRecord belongs_to :seller # 1,000円未満のItem scope :under1000, -> { where(“price < ?”, 1000) }
# 価格の降順
def self.desc_list
order(“items.price DESC”)
end
end
これでスコープの定義ができましたので、このスコープを利用する処理を作成します。ここでは、あまり複雑なことをせず、コントローラのindexメソッドを直接変更します。
まずは、1,000円未満のItemだけ表示してみましょう。
(app/controllers/items_controller.rb)
:
def index
# @items = Item.all
@items = Item.under1000
end
:
「localhost:3000/items」を再読込すれば、1,000円未満のItemだけのリストになっています。
降順表示を試すのは、以下の通りです。
(app/controllers/items_controller.rb)
:
def index
# @items = Item.all
@items = Item.desc_list
end
:
引数を渡す
前述で追加したスコープに、1,000円未満というスコープがあります。
このスコープは価格を元にして表示を絞り込むものですが、1,000円固定というのは、応用が利かなさすぎます。
そのため、一般的には、引数を使って金額部分を可変にするべきでしょう。
スコープ定義の構文にある通り、引数を追加することができますので、それで実現します。
(app/models/item.rb)
class Item < ApplicationRecord belongs_to :seller # 指定金額未満のItem scope :under_price, ->(price) { where(“price < ?”, price) } # 引数追加
# 価格の降順
def self.desc_list
order(“items.price DESC”)
end
end
では、20,000円未満のItemを表示してみましょう。
(app/controllers/items_controller.rb)
:
def index
# @items = Item.all
@items = Item.under_price(20000)
end
:
AND条件として使う
スコープを組み合わせることで、AND条件(両方のスコープの条件に合ったもの)に合致するレコードを取得できます。
その場合は、取得されたレコードに対してスコープを実行しますので、スコープを繰り返して利用する形になります。
「指定の価格以上」のスコープを追加して、100円よりも高くて20,000円未満のItem一覧を表示してみましょう。
(app/models/item.rb)
class Item < ApplicationRecord belongs_to :seller # 指定金額未満のItem scope :under_price, ->(price) { where(“price < ?”, price) } # 指定金額より大きいのItem scope :over_price, ->(price) { where(“price > ?”, price) }
# 価格の降順
def self.desc_list
order(“items.price DESC”)
end
end
次のようにして、100円よりも高くて20,000円未満の指定を、コントローラに施します。
(app/controllers/items_controller.rb)
:
def index
# @items = Item.all
@items = Item.under_price(20000).over_price(100)
end
:
joinして、他モデルのスコープを利用する
例えば、10,000円以上のItemを出品しているSellerだけを抽出したいような場合、Sellerのレコード取得時にItemのレコードにあるスコープを利用する必要があるでしょう。
そういった場合には、mergeメソッドを使ってスコープを利用します。
具体的な例を見た方が理解しやすいかと思います。
Sellerモデルにスコープを追加し、その中でItemのスコープを利用します。
(app/models/seller.rb)
class Seller < ApplicationRecord has_many :items # 1,0000円以上のItemを出品しているSeller scope :higher_seller, -> { joins(:items).merge(Item.over_price(10000)) }
end
Itemモデルのときと同じく、コントローラのindexメソッドの処理を変更します。
(app/controllers/sellers_controller.rb)
:
def index
# @sellers = Seller.all
@sellers = Seller.higher_seller
end
:
「localhost:3000/sellers」にアクセスしてみると、10,000円以上のItemを出品しているSellerのリストが表示されています。
※同じSellerが重複しているのは、複数のItemがマッチしているからです。この重複を解消するのは別の対応が必要ですので、ここでは割愛します。
https://web-camp.io/magazine/archives/12654
デフォルトスコープ
ここまでは、処理の中で都度スコープを使っていましたが、常にスコープが適用された状態にすることも可能です。
その場合は、scopeメソッドではなく、default_scopeメソッドを使用します。
例えば、Itemに追加したスコープdesc_listを常に適用されるデフォルトスコープにしてみます。
まず、通常はどうなっているのかを改めて確認するため、Itemコントローラのindexメソッドを元に戻しまして、確認しておきましょう。
(app/controllers/items_controller.rb)
:
def index
@items = Item.all
# @items = Item.under_price(20000).over_price(100)
end
:
では、デフォルトスコープを定義します。
(app/models/item.rb)
class Item < ApplicationRecord belongs_to :seller # 指定金額未満のItem scope :under_price, ->(price) { where(“price < ?”, price) } # 指定金額より大きいのItem scope :over_price, ->(price) { where(“price > ?”, price) }
# 価格の降順
deafult_scope :desc_list, -> { order(“items.price DESC”) } # デフォルトスコープに変更
end
この状態で、改めて「http://localhost:3000/items」を再読込してみましょう。
コントローラでスコープを利用していませんが、価格の降順になっています。
使用上の注意!
デフォルトスコープは、常に適用されるため、プログラムコードのあちこちでスコープを記載する手間が省けます。
しかし、ただ定義するだけで適用されますので、デフォルトスコープの存在に気付けず、思わぬ不具合の元になってしまうことがあります。
特に、複数人で開発しているときなどは、デフォルトスコープの存在を知らない人も出てきますので、その人が意図した表示にならないために解析に多くの時間を浪費してしまうことにもなりかねません。
デフォルトスコープの使用は、慎重に行うようにしましょう。
“未経験”でもたった1ヶ月で営業からエンジニアとして転職!『WebCamp』受講者インタビュー
まとめ
今回は、scopeについて説明しました。
scopeを利用することで、何度も同じクエリを作成する必要がなく、プログラムのメンテナンス性を上げ、可読性も改善することができます。
なにより、Railsの基本理念であるDRY(Don’t Repeat Yourself)を実現するためのとても協力な武器になりますので、積極的に使っていくようにしましょう。
・scopeを利用することで、クエリを定義して再利用することができる
・スコープの定義はモデルで行う
・スコープの定義はラムダ式を使う方法とメソッド形式を使う方法がある
・スコープの定義では、引数も使える
・スコープの定義を組み合わせることで、AND条件を表現できる
・他モデルのスコープを使う場合は、mergeを使う
・デフォルトスコープは常に適用されるため、慎重に使うこと
【インタビュー】1ヶ月でRubyをゼロから学び、Webエンジニアとして転職!
ブラジルから帰国し技術をつけようとRubyエンジニアを目指してWebCampでRubyを学び、見事Webエンジニアとして転職を果たした田中さんにお話を伺いました。
「Rubyの学習がしたい。基礎をしっかりと理解したい」
「転職のサポートがほしい」
と考えている方はぜひお読み下さい。
https://web-camp.io/magazine/archives/8535