Ruby
Rails
DDD

Railsのdefault_scopeをどうしても使いたい時

More than 1 year has passed since last update.


サマリ


  • default_scopeは便利

  • default_scopeは安直につかうとメンテ性が悪くなる

  • それでもdefault_scopeを使いたい

というときに、メンテ性を下げずにdefault_scopeを使う

(環境: Rails 4.2.5.1)


前提


default_scopeについて

ActiveRecordには、すべてのクエリに追加で絞り込みやorderを指定する default_scope という機能がある。

例えば、以下の例では、「disabled:trueなitemは常に除外する」という用例。

# app/models/item.rb

class Item < ActiveRecord::Base
default_scope { where(disabled: false) }
end

# itemsテーブルに id: 1, disabled: true なデータがあるとき
Item.find(1) # raises ActiveRecord::RecordNotFound


default_scopeが良くないとされる理由

最初どんなに「disabledなデータは絶対にクエリ検索結果から除外して大丈夫だ」と思っていたとしても、それは最初だけです。

例えば、管理画面からの呼び出しでは、disabledのチェックをつけ消しする必要があり、そうするとdisabledなデータも返さなければいけなくなります。

そのようなときは一応 unscope を使うとdefault_scopeを解除することができます。が、いかにもスパゲッティーコードが作られる臭いしかしませんね。

詳しくは触れませんが、http://stackoverflow.com/questions/25087336/why-is-using-the-rails-default-scope-often-recommend-against

での回答は的を射ていると思います。


それでもdefault_scopeを使いたい理由

defaultではない通常のscopeを使うことももちろん可能。

class Item < ActiveRecord::Base

scope :active, -> { where(disabled: false) }
end

しかし、最初の例では、管理画面以外のすべてのコードで disabledなItemの存在は消したいと考えていて、もしかしたら95%以上のコードではこのscopeを使うかもしれない。

そうすると毎度scopeをつけるのは面倒だったり、つけ忘れる危険性がある。


対処法

Itemにはdefault_scopeを適用せず、

新しいモデル ActiveItem を作り、そこにdefault_scopeを適用する。

その上で、最初の例で言えば、

管理画面からはItem、それ以外のほとんどすべてのコードではActiveItemを使う。

ActiveItem の作り方は以下の2つ


1. 普通にActiveRecord::Baseを継承し、テーブル名を指定

class ActiveItem < ActiveRecord::Base

self.table_name = 'items'
default_scope { where(disabled: false) }
end


2. Itemを継承

追記: これだとSingle Table Inheritanceの扱いになってしまうので別の方法をとる必要がある

(コメント欄)

後ほどきちんと調査して修正します。

class ActiveItem < Item

default_scope { where(disabled: false) }
end


1と2どっちがいいのか?

ドメインモデル的に考えると、継承する必要はないので、1のほうが良いと思う。

一方でDBとつなぐメソッドはだいたい両方で使いたくなる気がするので、継承したほうが楽。

DBとつなぐメソッドが多い or むしろドメインモデルはActiveRecordと別に分ける(最もかっちり) 場合は2を採用すれば良いかも。