「とある条件に見合う最初のデータを取ってきたい」としたときにクラスメソッドとscopeでは戻り値に違いがある。ということがわかったので今後の使い分けの参考に。
そのときやりたかったこと
ページに単純な定型文を表示させる機能を作った時のこと。条件としては、
- 本文がある(これを表示させる)
- 公開状況をON・OFFしてONのものを表示させる
- 公開状況がONであるデータが複数あったときにも一つだけが表示されるよう更新日時が一番新しいものを表示させるようにする
というもの。
それをページに表示させるため複数のデータの中から条件に合うデータを一つだけ持ってくるコードをそのモデルの中に書くことに。view側は単純で
- if @announcement.present?
= @anouncement.body
とし、そのデータがあるようであれば表示するという条件のみ。
scopeを使う
まず最初に使ったのがscope。書き方としては、
scope :published_latest, -> { where(published: true).order(updated_at: desc).first }
とした。意味としては、「公開済みのデータを取得し、そのデータ(複数)を更新日時が新しい順に並べ替え、その最初の一つ目を取得する」となる。しかし、この方法を使うと「該当するデータがない場合」にview側で不具合が出てしまう。
それは何故かというとscopeの戻り値は
ActiveRecord::Relationオブジェクト
となるため、もしデータがなかったとしても「からの配列が存在する」ということになり
- if @announcement.present?
が「true」になってしまうから。
クラスメソッドを使う
scopeでは該当データが存在しない場合エラーが出てしまうので、クラスメソッドを使うことに。コードはこんな感じ。
def self.published_latest
where(published: true).order(update_at: desc).first # 中身は一緒です
end
クラスメソッドを使うと、もし該当データが存在しない場合「nil」が返ることになり上記のview側のコードが期待通り「false」を返してくれる。同じ内容のコードでも戻り値の違いがあることに初めて気がついた。
結局は
ただ、結局は他のコードがscopeで統一されているのに一つだけクラスメソッドなのはおかしいのと、「publishedがtrueであること」と「更新日時を新しい順に並べる」というのは分けて書いた方がいいんではないか?ということになり
scope :published, -> { where(published: true) }
scope :order_by_recently, -> {order(updated_at: desc)}
とscopeを二つに切り分けて設定し、
@announcement = Announcement.published.order_by_recently.first
と、「first」もscopeの外に追い出すことによってview側の問題も解決することができた(最初からクラスメソッドを使う必要なんてなかった。)
しかし、二つの機能に違いがあることがわかったのでそれはいい勉強になったと思う。