LoginSignup
30
20

More than 5 years have passed since last update.

Railsのdefault_scopeは悪ではない。

Last updated at Posted at 2018-05-07

はじめに

default_scopeでググるとdefault_scopeは悪なので使うべきじゃないみたいな記事が多いのだけど、その後のアップデート情報が少なく、正しく使えば便利なのでその誤解を解きたい

Rails 5.1.6
Ruby 2.4.3
で確認

use case

質問投稿サイトを例にすると、
質問モデルがあり、ステータスとして表示、非表示のものがあるとします。
基本的にはステータスが表示のものだけを表示する。ただし、投稿者のMyPageや管理画面では全部を表示したい。
この場合、非表示のものが間違って表示されてはいけないのでステータスが表示の条件を当てるようにするが、毎回指定するのは面倒だし、漏れが出てしまう可能性も十分にあります。
そこでdefault_scopeを使います。
挙動としては、デフォルトでは表示のものだけを取得で、指定された場合だけその条件を外すようにする。

モデル例は以下のようになります。


class Question < ApplicationRecord
  enum status: {active: 1,inactive: 10}
  default_scope { where(status: :active) }
end

デフォルトでのSQLは以下のようになる


irb(main):067:0> Question.all.to_sql
=> "SELECT `questions`.* FROM `questions` WHERE `questions`.`status` = 1"

default_scopeの解除

default_scopeの解除でunscopedがよく記事に出てますが、これを使うとたしかに危険です。


irb(main):068:0> Question.unscoped.to_sql
=> "SELECT `questions`.* FROM `questions`"

一見いいようにみえますが、例えばacts_as_paranoidを使っていたとします。
モデルにacts_as_paranoidを追加


class Question < ApplicationRecord
  acts_as_paranoid
  enum status: {active: 1,inactive: 10}
  default_scope { where(status: :active) }
end

デフォルトでのSQLは以下のようになる


irb(main):070:0> Question.all.to_sql
=> "SELECT `questions`.* FROM `questions` WHERE `questions`.`deleted_at` IS NULL AND `questions`.`status` = 1"

unscopedすると。。

irb(main):072:0> Question.unscoped.to_sql
=> "SELECT `questions`.* FROM `questions`"

deleted_at IS NULL まで解除されてしまっています。

unscopeを使う


class Question < ApplicationRecord
  acts_as_paranoid
  enum status: {active: 1,inactive: 10}
  default_scope { where(status: :active) }
  scope :with_inactive, ->{ unscope(where: :status) } # ←これ追加
end

unscopeを利用してstatusの条件を外します。
結果は以下のように

irb(main):074:0> Question.with_inactive.to_sql
=> "SELECT `questions`.* FROM `questions` WHERE `questions`.`deleted_at` IS NULL"

deleted_at IS NULLの条件はそのままに、status: :activeだけ外れています。

default_scopeでorderを指定している場合は?

default_scopeでorderを指定するケースはあんまり思い浮かばないですが、同じように以下のようにすると外すことができます
モデル

class Question < ApplicationRecord
  acts_as_paranoid
  enum status: {active: 1,inactive: 10}
  default_scope { where(status: :active) }
  default_scope { order(created_at: :desc) } # ←これ追加
end

デフォルトでorder byがつく

irb(main):078:0> Question.all.to_sql
=> "SELECT `questions`.* FROM `questions` WHERE `questions`.`deleted_at` IS NULL AND `questions`.`status` = 1 ORDER BY `questions`.`created_at` DESC"

そこでunscope(:order)する。※unscopeにhashが渡せるのはwhereだけ
これでorder byが外れます

irb(main):087:0>  Question.unscope(:order).to_sql
=> "SELECT `questions`.* FROM `questions` WHERE `questions`.`deleted_at` IS NULL AND `questions`.`status` = 1"

その他にも指定できます
http://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-unscope

モデルをイニシャライズするときにデフォルト値が入ってしまう

以下のようにしているとnewしたときにデフォルト値が自動で入ってしまう

class Question < ApplicationRecord
  enum status: {active: 1,inactive: 10}
  default_scope { where(status: :active) }
end
irb(main):088:0> Question.new
=> #<Question id: nil, user_id: nil, status: "active", ...>

unscopeしてあげれば解決しますが、ちょっと面倒ですね

irb(main):105:0> Question.unscope(where: :status).new
=> #<Question id: nil, user_id: nil, status: nil, 

attributeでdefault設定すればいけるかなとおもいきや、だめだった

class Question < ApplicationRecord
  enum status: {active: 1,inactive: 10}
  default_scope { where(status: :active) }
  attribute :status, :integer, default: -> { :inactive } # ←これ追加してもdefaultは:active
end

ここはデフォルト値と違う値を入れたい場合はnewの後に設定してあげるでいいんじゃないかな。

終わりに

default_scopeは便利なので使うといいよ。ただしunscopedではなくunscopeを利用

30
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
20