サマリ
- 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を採用すれば良いかも。