読者です 読者をやめる 読者になる 読者になる

ボクココ

サービス開発を成功させるまでの歩み

ActiveRecordにおけるScopeのつかいどき

Rails

今回は ActiveRecord の機能の紹介。

読者の方で以下のようなRailsコードを書いたことがないだろうか?

class ArticleController < ActionController::Base
  before_action :fetch_article, only: [:show, :edit, :update, :destroy]

  def ...
  end

  private
  def fetch_article
    @article = Article.find(params[:id])
  end
end

このようなコードは お得意の Scaffold を使うと自動生成されるので、正しいと思いがちだ。実際に運用する場面になった時、どこが問題だかわかるだろうか。

他ユーザーに記事を編集させない

問題は、Scaffoldがログインのことを考えていないサンプルであるため、誰でも読み書きできてしまう点にある。先ほどのコードで、例えば記事を読んでいる人が記事を編集できたり削除できたりしたらもちろん困ることだろう。

ではどう対応するか。以下に示す。 User と Article が1対多のhas_manyな関係であることを前提とする。

  def fetch_article
    @article = current_user.articles.find(params[:id])
  end

このようにすれば他のユーザーが記事にアクセスした際には 404 を返すようになる。(showの時は他ユーザーでも見れた方がいいので、showは前のやり方で書く。)Rails では基本的に id がインクリメンタルな数字なので、バックドアを作りやすい。認可が必要なサービスでは必ず注意するようにしよう。

さて、このように ActiveRecord では Article.findだけでなく、テーブルの関連がある場合は current_user.articles.findと書ける。これは大変便利な機能である。

Scope で書けば関連を考慮した自作メソッドが作れる

例えば、ユーザーがクッキー情報に記事idを持っていて、それがあればその記事を参照、なければ最新の記事を参照といったメソッドを作ろうとしよう。

class Article < ActiveRecord::Base
  def self.find_my_article article_id
     article_id.nil? ? self.first : self.where(id: article_id).first || self.first 
  end
end

ちょっとわかりにくいサンプルかもしれないが、解説すると 引数で渡された article_id がnilなら1個目を返す。nilじゃなければそのarticle_idからArticleをとってくる。それがなかったら場合も1個目も返す。 Cookie の値を信用してはならないため、最後の || self.first も必要となる。

こう書けば、 Article.find_my_article(cookies[:my_article_id]) と書ける。が、これだと先ほどの問題のように、他のユーザーも自分の記事を編集できたり削除できたりできるようになってしまう。ここでScopeを使おう。改善したのが以下のコード。

class Article < ActiveRecord::Base
  scope :find_my_article -> article_id { article_id.nil? ? self.first : self.where(id: article_id).first || self.first } 
end

これはRubyのラムダ式といい、処理をProcの形式で表現する。ちょっとわかりにくいかもしれないが、感覚としては クラスメソッド(def self.)と同様な気持ちで書ける。このように書けば、 current_user.articles.find_my_article(cookies[:my_article_id]) と書けて、安全なコードを実現できた。

追記: さらにチェーンを発生させるようにするには、上記例のようにラムダ式内でオブジェクトが一つに返すようなメソッドを返すようにするのではなく、where などのActiveRecord::Relationを返すようにすると連続でスコープが記述できる。本来はそのような使い方の方が正しいようだ。

まとめ

Scopeの使いどきとしてはこのようにモデルをまたいだクエリを投げた時にその後のクエリをカスタマイズしたい時に使う。

ほとんどのサンプルでこういったことが書かれていないため、Scopeの必要性がわかりにくかった(scope :recent とかだけのサンプル)。本記事でScopeの必要性がわかっていただけたら幸いである。