【Rails】Model(モデル)のscope(スコープ)ってなんだろう?を解決する

モデルにおけるscopeってなんですか?

Railsを学習していて次のようなコードを発見しました。「モデルのscopeってなんだろう?」ということで調べて記事にしました。

scope :name, ->(name) { where(name: name) }


ひとまずRailsガイドで調べてみました。Rails ガイド Active Record - 14.スコープ

スコープを設定することで、関連オブジェクトやモデルへのメソッド呼び出しとして参照される、よく使用されるクエリを指定することができます。スコープではwherejoinsincludesなど、これまでに登場したすべてのメソッドを使用できます。どのスコープメソッドも、常にActiveRecord::Relationオブジェクトを返します。このオブジェクトに対して、別のスコープを含む他のメソッド呼び出しを行うこともできます。


Railsガイドを読むとなんとなく分かると思いますが、次のようなArticleモデル内に「公開済み」となっている記事をwhereを用いて取得するクラスメソッドがあるとします。

class Article < ApplicationRecord

  def self.published
    where(published: true)
  end
end

上のようなクラスメソッドをscopeメソッドを用いることで次のように書き換えることができます。書き換えているだけでクラスメソッドの定義と完全に同じなのでscopeを利用するか、クラスメソッドで定義するかは好みの問題のようです。

class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
end

また、次のようにすることでスコープをスコープ内でチェインさせることも出来るみたいです。

class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :published, -> { published.where("comments_count > 0") }
end

上で定義したスコープを呼び出すためには、クラスにて次のようにすることで呼び出しができるようです。

Article.published

また、関連付けされているクラスからスコープを呼び出すことも出来ます。

category = Category.first
category.articles.publiished

ここまでは、Railsガイドをそのまま読んだだけなので、実際にRailsでプロジェクトを作成してscopeを使ってみます。




実際にscopeを使ってみる

まずは次のようなテーブルとデータを作成しました。



Users

id name
1 山田太郎


Posts

id title body published user_id
1 山田太郎の記事1 これは山田太郎の初めての記事です。 true 1
2 山田太郎の記事2 これは山田太郎の2回目の記事です。 true 1
3 山田太郎の非公開記事 これは山田太郎の非公開記事です。 false 1



公開済みとなっているブログ記事のみを取得して一覧表示する想定なので、Postモデルでscopeを設定します。

app/models/post.rb

class Post < ApplicationRecord
  belongs_to :user

  # 公開済みとなっている記事を取得
  scope :published, -> { where(published: true) }
end


それではコントローラ側で取得したブログ記事を返します。今回はRailsをAPIモードで作成しているため、json形式でブログ記事を返します。

app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def index
    # モデル側で定義したpublishedを呼び出す
    posts = Post.published
    render json: { data: posts }
  end
end


postmanなどで正常に公開済みの記事がレスポンスとして返ってきているか来ているか確認してみます。

{
    "data": [
        {
            "id": 1,
            "title": "山田太郎の記事1",
            "body": "これは山田太郎の初めての記事です。",
            "user_id": 1,
            "created_at": "2020-09-14T13:23:52.206Z",
            "updated_at": "2020-09-14T13:23:52.206Z",
            "published": true
        },
        {
            "id": 2,
            "title": "山田太郎の記事2",
            "body": "これは山田太郎の二回目の記事です。",
            "user_id": 1,
            "created_at": "2020-09-14T13:23:52.216Z",
            "updated_at": "2020-09-14T13:23:52.216Z",
            "published": true
        }
    ]
}

無事にpublished: true(公開済み)のブログ記事が返ってきました。



降順への並び替えをscopeで同時にやってみる 

次はレスポンスとして返ってきているブログ記事一覧をcreated_atで降順にしてみます。

app/models/post.rb

class Post < ApplicationRecord
  belongs_to :user

  scope :published, -> { where(published: true).order(created_at: :DESC) }
end

orderを追記しcreated_at: :DESCで降順にして取得するようにして結果を確認してみます。

{
    "data": [
        {
            "id": 2,
            "title": "山田太郎の記事2",
            "body": "これは山田太郎の二回目の記事です。",
            "user_id": 1,
            "created_at": "2020-09-14T13:23:52.216Z",
            "updated_at": "2020-09-14T13:23:52.216Z",
            "published": true
        },
        {
            "id": 1,
            "title": "山田太郎の記事1",
            "body": "これは山田太郎の初めての記事です。",
            "user_id": 1,
            "created_at": "2020-09-14T13:23:52.206Z",
            "updated_at": "2020-09-14T13:23:52.206Z",
            "published": true
        }
    ]
}


seedでデータを作成しているため、作成日時がほとんど同じなので分かりづらいと思うので、新たにデータを追加してみます。

$ rails c
> user = User.first # => 山田太郎
> Post.create(title: "降順チェック", body: "降順で取得できるかチェックします。", published: true, user_id: user.id)

新たに公開済みのブログ記事として作成できたので、確認してみます。

{
    "data": [
        {
            "id": 3,
            "title": "降順チェック",
            "body": "降順で取得できるかチェックします。",
            "user_id": 1,
            "created_at": "2020-09-14T14:23:27.174Z",
            "updated_at": "2020-09-14T14:23:27.174Z",
            "published": true
        },
        {
            "id": 2,
            "title": "山田太郎の記事2",
            "body": "これは山田太郎の二回目の記事です。",
            "user_id": 1,
            "created_at": "2020-09-14T13:23:52.216Z",
            "updated_at": "2020-09-14T13:23:52.216Z",
            "published": true
        },
        {
            "id": 1,
            "title": "山田太郎の記事1",
            "body": "これは山田太郎の初めての記事です。",
            "user_id": 1,
            "created_at": "2020-09-14T13:23:52.206Z",
            "updated_at": "2020-09-14T13:23:52.206Z",
            "published": true
        }
    ]
}

降順で表示されていることが分かります。



クラスメソッドとscopeで違いはないのか確認する

スコープ

scope :published, -> { where(published: true).order(created_at: :DESC) }

# スコープで取得したときのSQL文
SELECT `posts`.* FROM `posts` WHERE `posts`.`published` = TRUE ORDER BY `posts`.`created_at` DESC

クラスメソッド

def self.published
  where(published: true).order(created_at: :DESC)
end

# クラスメソッドで取得したときのSQL文
SELECT `posts`.* FROM `posts` WHERE `posts`.`published` = TRUE ORDER BY `posts`.`created_at` DESC

同じSQL文が実行されているようです。



スコープで引数を渡す

見つけたコードが次のような引数を渡して取得するコードだったので、引数を渡す方法も確認してみたいと思います。

scope :name, ->(name) { where(name: name) }


スコープの引数で受け取った文字列で部分一致検索をする

次のようなRequestを受け取ったとき、パラメータを引数で受け取り検索するスコープを定義します。

{
  "title": "山田"
}


scopesearchメソッドを定義して引数を受け取り、Postsテーブルのtitleカラムと部分一致するデータを取得します。

app/models/post.rb

class Post < ApplicationRecord
  belongs_to :user

  scope :search, ->(title) { where("title LIKE?", "%#{title}%") }
end

ストロングパラメータを設定して、モデル側で作成したsearchメソッドに渡しています。

app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def index
    # 引数として受けとったパラメータを渡す
    posts = Post.search(post_params[:title])

    render json: { data: posts }
  end

  private

  # ストロングパラメータを追記
  def post_params
    params.require(:post).permit(:title)
  end
end


jsonで送信した文字列で部分一致検索出来ているか確認します。

  • 結果
{
    "data": [
        {
            "id": 1,
            "title": "山田太郎の記事1",
            "body": "これは山田太郎の初めての記事です。",
            "user_id": 1,
            "created_at": "2020-09-14T13:23:52.206Z",
            "updated_at": "2020-09-14T13:23:52.206Z",
            "published": true
        },
        {
            "id": 2,
            "title": "山田太郎の記事2",
            "body": "これは山田太郎の二回目の記事です。",
            "user_id": 1,
            "created_at": "2020-09-14T13:23:52.216Z",
            "updated_at": "2020-09-14T13:23:52.216Z",
            "published": true
        },
        {
            "id": 3,
            "title": "山田太郎の非公開記事",
            "body": "これは山田太郎の非公開記事です。",
            "user_id": 1,
            "created_at": "2020-09-14T13:23:52.227Z",
            "updated_at": "2020-09-14T13:23:52.227Z",
            "published": false
        }
    ]
}

無事titleに「山田」と含まれているブログ記事のみ取得することができました。



まとめ

  • モデルにおけるscopeを用いることでクラスメソッドを書き換えることができる
  • スコープで引数を渡すことができる
  • scopeで定義したスコープメソッドとクラスメソッドが実行するSQL文が同じ
  • スコープメソッドは常にActiveRecord::Relationを返す

Written by@Ryutaro
日々学習した技術系のアウトプットをしていきます。学習内容: Ruby, Ruby on Rails, Go, TypeScript, Docker

GitHub