更新日:
【Rails】 モデルのスコープ機能(scope)の使い方を1から理解する
モデルのスコープ機能とは、モデル側であらかじめ特定の条件式に対して名前をつけて定義し、その名前でメソッドの様に条件式を呼び出すことが出来る仕組みのことです。
1
2
3
class モデル名 < ApplicationRecord
scope :スコープの名前, -> { 条件式 }
end
例えば、publishedという名前に公開記事を取得する条件式を定義するには、以下のように記述します。
1
2
3
class Blog < ApplicationRecord
scope :published, -> { where(published: true) }
end
定義したスコープは以下のように呼び出す事が出来ます。
1
Blog.published
スコープ機能の基礎知識
この章では、モデルのスコープ機能の基礎知識について解説します。
スコープ機能のメリットとは?
スコープ機能を使う事によって、下記の様なメリットがあります。
- 条件式に名前を付けられるので、直感的なコードになる
- 修正箇所を限定することが出来る
- コードが短くなる
記事を管理しているblogsテーブルを使って、スコープ機能のメリットを確認していきましょう。
blogsテーブルから公開記事を取得するには、Blogモデルに対してwhere(published: true)
の条件式を使います。この条件式に対して、published
の名前を付けてscopeメソッドで定義すると下記の様になります。
1
2
3
class Blog < ApplicationRecord
scope :published, -> { where(published: true) }
end
このpublished
のスコープを使って、スコープを使用した場合・使用しなかった場合を比較していきます。
1. 条件式に名前を付けられるので、直感的なコードになる
スコープ機能を使うと、条件式に対して名前を付けられるので直感的なコードになります。
先ほど定義したスコープによって、公開記事を取得するwhere(published: true)
の条件式をpublished
の名前で呼び出すことが出来ます。
1
Blog.where(published: true)
1
Blog.published
スコープ機能を使うことによって、コードが直感的で簡潔になっていることが分かります。
2. 修正箇所を限定することが出来る
条件式に変更が出た場合に、修正がscopeメソッドで定義した箇所だけで済みます。
例えば、公開記事を取得する上限を2件までにしたい場合は、下記の様にscopeメソッドで定義した箇所にlimit(2)
を加えるだけで済みます。
1
2
3
class Blog < ApplicationRecord
scope :published, -> { where(published: true).limit(2) } # limitを追加
end
scopeメソッドを使わない場合は、where(published: true)
の全ての箇所にlimit
を追加する必要があります。
1
2
3
4
5
6
7
def index
@published_blogs = Blog.where(published: true).limit(2) # limitを追加
end
def test
@published_blogs = Blog.where(published: true).limit(2) # limitを追加
end
3. コードが短くなる
スコープ機能は、クラスメソッドでも代用可能ですが、scopeメソッドで定義すると1行でコードを記述することが出来ます。
1
2
3
4
class Blog < ApplicationRecord
# この1行で済む
scope :published, -> { where(published: true).limit(2) }
end
クラスメソッドで定義すると、最低3行は必要になります。
1
2
3
4
5
6
class Blog < ApplicationRecord
# 最低3行は必要
def self.published
where(published: true).limit(2)
end
end
それでは、scopeメソッドの定義方法と基本的な使い方を解説していきます。
scopeメソッドの基本的な使い方
scopeメソッドの第一引数に条件式を呼び出すための名前、第二引数に条件式を実装するlambdaを渡します。
1
2
3
class モデル名 < ApplicationRecord
scope :スコープの名前, -> { 条件式 }
end
- 第一引数は、シンボル(:)を使ってスコープの名前を指定します。
- 第二引数の
->{条件式}
のlambdaは、処理の中でメソッドを定義してくれる手法です。
※第二引数のlambdaは、->{}
の中に条件式を記述することが出来るとだけ覚えておきましょう。
引数を使う
scopeメソッドは、下記の様に->
の後に引数を定義することで、引数を渡すことが出来ます。
1
2
3
class モデル名 < ApplicationRecord
scope :スコープの名前, -> (引数){ 条件式 }
end
先ほど公開記事を取得する際には、limit(2)
によって最大2つまでの取得に固定されていましたが、取得する記事の上限をその都度変更したい場合は、下記の様に引数countをscopeメソッドに定義します。
1
2
3
4
5
6
7
class Blog < ApplicationRecord # 引数を使用しない場合
scope :published, -> { where(published: true).limit(2) }
end
class Blog < ApplicationRecord # 引数を使用した場合
scope :published, -> (count){ where(published: true).limit(count) }
end
これによって、Blog.published(値)
で公開記事の最大取得数を変更することが出来ます。引数に3を渡して実行してみましょう。
1
2
3
4
5
6
7
Blog.published(3)
SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`published` = 1 LIMIT 3
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 1, url: "ruby", title: "rubyとは?", published: true, created_at: "2019-12-05 15:08:25", updated_at: "2019-12-05 15:08:25">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">,
#<Blog id: 4, url: "gem", title: "gemとは?", published: true, created_at: "2019-12-08 15:30:00", updated_at: "2019-12-08 15:30:00">]>
返り値で公開記事を3件取得出来ているのが確認できます。この様に引数を使うことで、汎用性のある条件式を組み立てることが出来ます。
メリットの所でもscopeメソッドで定義すると「修正する箇所を限定出来る」と解説しましたが、scopeメソッドを使わないで条件式を変更する場合は、全ての箇所の修正が必要になります。
もしscopeメソッドを使わないで、複数の箇所に同じ条件式を記述した場合は、
1
2
3
4
5
def index
Blog.where(published: true).limit(3)
Blog.where(published: true).limit(3)
Blog.where(published: true).limit(3)
end
となりますが、この条件式に降順で取得するorder(id: desc)
を追加したければ1つ1つのBlog.where(published: true).limit(3)
に対して
1
2
3
4
5
def index
Blog.where(published: true).limit(3).order(id: desc) # orderメソッド追加
Blog.where(published: true).limit(3).order(id: desc) # orderメソッド追加
Blog.where(published: true).limit(3).order(id: desc) # orderメソッド追加
end
にする必要があります。
しかし、scopeメソッドで定義してあれば、
1
2
3
class Blog < ApplicationRecord
scope :published, -> (count){ where(published: true).limit(count).order(id: desc) # ここに追加するだけ}
end
の様に1箇所の修正で済みます。scopeメソッドで定義すると修正箇所が限定されて、修正漏れを防ぎやすくなります。
条件文を使う
scopeメソッドは、if
文を使って処理を分岐することが出来ます。例えば、blogの件数は現在5件しかありませんので、最大取得記事数を5未満に制限します。
下記は、引数で渡される値が5未満の場合はその値の記事数を取得する事が出来て、5を超える場合は5件以上はないので全件(5件)取得します。
1
2
3
class Blog < ApplicationRecord
scope :published, -> (count){ where(published: true).limit(count) if count < 5 }
end
引数に2を渡した場合は、2つの公開記事のデータを取得する事が出来ています。しかし、引数に50を渡した場合は、全件取得されている事が確認出来ます。
1
2
3
4
5
6
Blog.published(2)
SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`published` = 1 LIMIT 2
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 1, url: "ruby", title: "rubyとは?", published: true, created_at: "2019-12-05 15:08:25", updated_at: "2019-12-05 15:08:25">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">]>
1
2
3
4
5
6
7
8
9
Blog.published(50)
SELECT `blogs`.* FROM `blogs`
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 1, url: "ruby", title: "rubyとは?", published: true, created_at: "2019-12-05 15:08:25", updated_at: "2019-12-05 15:08:25">,
#<Blog id: 2, url: "bundle", title: "bundlerとは?", published: false, created_at: "2019-12-06 15:10:05", updated_at: "2019-12-06 15:10:05">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">,
#<Blog id: 4, url: "gem", title: "gemとは?", published: true, created_at: "2019-12-08 15:30:00", updated_at: "2019-12-08 15:30:00">,
#<Blog id: 5, url: "scope", title: "scopeとは?", published: false, created_at: "2019-12-09 11:08:00", updated_at: "2019-12-09 11:08:00">]>
引数に50を渡した場合に全件取得していた理由は、scopeメソッドは条件文が評価された結果がfalseを返す時はallメソッドを返して全てのレコードを取得するからです。(後ほど詳しく解説します。)
スコープ機能の応用知識
この章では、スコープ機能の応用的な使い方からクラスメソッドとの違いについて解説します。
scopeメソッドの応用的な使い方
scopeメソッドは、下記の様にスコープに対してメソッドチェーンを使って他のクエリメソッドや別のスコープを呼び出すことが出来ます。
1
2
3
4
5
6
Blog.published.order(id: :desc)
SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`published` = 1 ORDER BY `blogs`.`id` DESC LIMIT 2
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 4, url: "gem", title: "gemとは?", published: true, created_at: "2019-12-08 15:30:00", updated_at: "2019-12-08 15:30:00">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">]>
scopeメソッドの応用的な使い方をマスターする為に、メソッドチェーンや返り値を理解していきましょう。
メソッドチェーンについて
メソッドチェーンは、あるオブジェクトに対して.
(ドット)でメソッドを繋げます。そして、.
(ドット)の次に繋げたメソッドが .
(ドット)の前のメソッドの返り値を受け取って処理していきます。
1
Blog.published.order(id: :desc)
上記のコードをメソッドチェーンの流れに当てはめると、下記の意味になります。
- Blogオブジェクトに対して
.
(ドット)でpublished
スコープを繋げます。 .
(ドット)の前のpublished
スコープで返り値を.
(ドット)の次に繋げたorderメソッドが受け取って処理していきます。
メソッドチェーンは、返り値がActiveRecord::Relationオブジェクトに対して使うことが出来ます。
返り値について(ActiveRecord::Relation)
scopeメソッドの返り値は、常にActiveRecord::Relationオブジェクトを返します。
下記のpublished
スコープを実行すると、ActiveRecord::Relationオブジェクト
が返り値となっている事が確認出来ます。
1
2
3
4
5
6
7
Blog.published(3)
SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`published` = 1 LIMIT 3
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 1, url: "ruby", title: "rubyとは?", published: true, created_at: "2019-12-05 15:08:25", updated_at: "2019-12-05 15:08:25">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">,
#<Blog id: 4, url: "gem", title: "gemとは?", published: true, created_at: "2019-12-08 15:30:00", updated_at: "2019-12-08 15:30:00">]>
scopeメソッドの最大の特徴は、scopeメソッドの返り値がActiveRecord::Relationオブジェクトになる事なので覚えておきましょう。
nilの場合について
scopeメソッドは、scopeメソッドで定義した条件式がnilを返す場合は、allメソッドを実行します。allメソッドの返り値は、ActiveRecord::Relationオブジェクトです。
つまり、scopeメソッドで定義した条件式がnilを返す場合でも、ActiveRecord::Relationオブジェクトを返します。(※条件式の中のif文がfalseを返す場合も同様の挙動です。)
publishedスコープの条件式をnil
に変更してBlog.published
を実行すると、blogsテーブルの全レコードが取得されます。
1
2
3
class Blog < ApplicationRecord
scope :published, -> { nil } # 条件式をnilに変更
end
1
2
3
4
5
6
7
8
9
Blog.published
SELECT `blogs`.* FROM `blogs`
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 1, url: "ruby", title: "rubyとは?", published: true, created_at: "2019-12-05 15:08:25", updated_at: "2019-12-05 15:08:25">,
#<Blog id: 2, url: "bundle", title: "bundlerとは?", published: false, created_at: "2019-12-06 15:10:05", updated_at: "2019-12-06 15:10:05">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">,
#<Blog id: 4, url: "gem", title: "gemとは?", published: true, created_at: "2019-12-08 15:30:00", updated_at: "2019-12-08 15:30:00">,
#<Blog id: 5, url: "scope", title: "scopeとは?", published: false, created_at: "2019-12-09 11:08:00", updated_at: "2019-12-09 11:08:00">]>
scopeメソッドの条件式がnilを返す場合でもActiveRecord::Relationオブジェクトを返すので、scopeメソッドで定義したスコープに対して、常にメソッドチェーンを使う事が出来ます。
1
2
3
4
5
6
7
8
9
Blog.published.order(id: :desc)
SELECT `blogs`.* FROM `blogs` ORDER BY `blogs`.`id` DESC
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 5, url: "scope", title: "scopeとは?", published: false, created_at: "2019-12-09 11:08:00", updated_at: "2019-12-09 11:08:00">,
#<Blog id: 4, url: "gem", title: "gemとは?", published: true, created_at: "2019-12-08 15:30:00", updated_at: "2019-12-08 15:30:00">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">,
#<Blog id: 2, url: "bundle", title: "bundlerとは?", published: false, created_at: "2019-12-06 15:10:05", updated_at: "2019-12-06 15:10:05">,
#<Blog id: 1, url: "ruby", title: "rubyとは?", published: true, created_at: "2019-12-05 15:08:25", updated_at: "2019-12-05 15:08:25">]>
上記のコードはエラーが起きる事なく、publishedのスコープに対してメソッドチェーンでorderメソッドを呼び出す事が出来ています。
メソッドチェーンで連結して、データを取得してみよう
先ほどはpublished
のスコープにメソッドチェーンでorderメソッドを呼び出していましたが、別のスコープも呼び出す事も出来ます。recent
・search_with_title
のスコープを追加して確認してみましょう。
1
2
3
4
5
class Blog < ApplicationRecord
scope :recent, -> { order(id: :desc) }
scope :published, ->(count) { where(published: true).limit(count) if count < 5 }
scope :search_with_title, ->(title) { where(title: title) }
end
1
2
3
4
5
6
Blog.published(2).search_with_title(['rubyとは?', 'gemとは?']).recent
SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`published` = 1 AND `blogs`.`title` IN ('rubyとは?', 'gemとは?') ORDER BY `blogs`.`id` DESC LIMIT 2
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 4, url: "gem", title: "gemとは?", published: true, created_at: "2019-12-08 15:30:00", updated_at: "2019-12-08 15:30:00">,
#<Blog id: 1, url: "ruby", title: "rubyとは?", published: true, created_at: "2019-12-05 15:08:25", updated_at: "2019-12-05 15:08:25">]>
複数のスコープを連結していますが、発行されるSQLは1回のみです。
これはクエリメソッドの遅延ロードと呼ばれるもので、データが必要になって初めてデータベースに問い合わせるからです。1回のSQLの発行で済むのでパフォーマンスも良くなります。
1
2
3
4
SELECT `blogs`.*
FROM `blogs`
WHERE `blogs`.`published` = 1 AND `blogs`.`title` IN ('rubyとは?', 'gemとは?')
ORDER BY `blogs`.`id` DESC LIMIT 2
上記のSQLのWHERE句では、blogs.published = 1
とblogs.title IN ('rubyとは?', 'gemとは?')
の間に条件を絞り込むAND
があります。ANDは、どちらの条件も満すという意味で、「published列の値が1(true)」かつ「タイトルが'rubyとは?'と 'gemとは?'」の条件を満たしたレコードを取得します。
このようにメソッドチェーンでメソッドや別のスコープを呼び出すことで、より複雑なデータ検索ができます。
スコープとクラスメソッドの違い
ここまでscopeメソッドの定義方法について解説してきましたが、クラスメソッドでも同じように定義できます。「対象モデル.クラスメソッド名」で定義した条件式を呼び出す事が出来ます。
1
2
3
4
5
class モデル名 < ApplicationRecord
def self.メソッド名
条件式
end
end
scopeメソッドとクラスメソッドは、nilを返す時の挙動が違います。
- scopeメソッドはnilの場合に
allメソッド
が実行されるので、メソッドチェーンを使う事ができる - クラスメソッドはnilの場合は
nil
が返るので、メソッドチェーンを使う事が出来ない。
それでは、クラスメソッドの返り値やnilの場合の挙動を確認していきましょう。
返り値について(ActiveRecord::Relation)
クラスメソッドで定義した条件式が正常に評価された場合は、ActiveRecord::Relationオブジェクトが返ります。publishedクラスメソッドに、最大2つの公開記事を取得する条件式を定義してBlog.published
を実行していきます。
1
2
3
4
5
class Blog < ApplicationRecord
def self.published
where(published: true).limit(2)
end
end
1
2
3
4
5
6
Blog.published
SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`published` = 1 LIMIT 2
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 1, url: "ruby", title: "rubyとは?", published: true, created_at: "2019-12-05 15:08:25", updated_at: "2019-12-05 15:08:25">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">]>
返り値は、ActiveRecord::Relationオブジェクトになります。返り値がActiveRecord::Relationオブジェクトなので、publishedクラスメソッドに対してもメソッドチェーンを使って他のメソッドや別のスコープを呼び出す事が可能です。
1
2
3
4
5
6
Blog.published.order(id: :desc)
SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`published` = 1 ORDER BY `blogs`.`id` DESC LIMIT 2
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 4, url: "gem", title: "gemとは?", published: true, created_at: "2019-12-08 15:30:00", updated_at: "2019-12-08 15:30:00">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">]>
この様にクラスメソッドでも、返り値がActiveRecord::Relationオブジェクトの場合はメソッドチェーンを使う事が出来ます。
nilの場合について
クラスメソッドで定義した条件式がnilを返す場合の返り値は、nilが返るのでメソッドチェーンを使う事が出来ません。この状態でメソッドチェーンを使うとNoMethodError
が発生します。
publishedクラスメソッドの条件式をnilに変更してBlog.published
を実行します。
1
2
3
4
5
class Blog < ApplicationRecord
def self.published
nil # 変更
end
end
1
2
Blog.published
=> nil # 返り値
コード実行後の返り値は、nilが返ります。この状態で、メソッドチェーンを使ってorderメソッドを呼び出してみましょう。
1
2
Blog.published.order(id: :desc)
NoMethodError: undefined method `order' for nil:NilClass
返り値がnilに対してorderメソッドを実行しているので、NoMethodError
が発生します。この様にscopeメソッドで定義した時とは違い、クラスメソッドはロジックがnilを返す場合は、nilが返るのでメソッドチェーンを使う場合は注意が必要です。
引数を使う場合は、クラスメソッドの方が推奨される
scopeメソッドの引数は、クラスメソッドの機能を複製させたものなので、引数を使う場合はクラスメソッドを使う方が推奨されます。(Ruby on Rails Guidesより)
publishedクラスメソッドに引数countを追加して、取得する記事数を変更できる様にします。最大3つの公開記事のデータを取得してみましょう。
1
2
3
4
5
class Blog < ApplicationRecord
def self.published(count)
where(published: true).limit(count)
end
end
1
2
3
4
5
6
7
Blog.published(3)
SELECT `blogs`.* FROM `blogs` WHERE `blogs`.`published` = 1 LIMIT 3
=> #<ActiveRecord::Relation [ # 返り値
#<Blog id: 1, url: "ruby", title: "rubyとは?", published: true, created_at: "2019-12-05 15:08:25", updated_at: "2019-12-05 15:08:25">,
#<Blog id: 3, url: "sketch", title: "sketchの使い方", published: true, created_at: "2019-12-07 17:10:55", updated_at: "2019-12-07 17:10:55">,
#<Blog id: 4, url: "gem", title: "gemとは?", published: true, created_at: "2019-12-08 15:30:00", updated_at: "2019-12-08 15:30:00">]>
しかし、クラスメソッドは前述した様にロジックがnilを返す場合はscopeメソッドとは違いnilが返ってきます。もしメソッドチェーンを使っていればNoMethodErrorのエラーが発生する場合があるので注意してください。
この記事のまとめ
- scopeメソッドを使うと、条件式に名前を付けてメソッドの様に呼び出すことが出来る
- scopeメソッドの返り値は、ActiveRecord::Relationオブジェクトを常に返す
- クラスメソッドでロジックがnilやfalseを返す場合は、返り値がnilになるのでメソッドチェーンを使う事が出来ない