注意預設範圍

ActiveRecord 包含 default_scope,預設情況下自動調整模型範圍。

class Post
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

上面的程式碼將提供在你對模型執行任何查詢時已釋出的帖子。

Post.all # will only list published posts 

該範圍雖然看起來無害,但卻有許多你可能不想要的隱藏副作用。

default_scopeorder

由於你在 default_scope 中宣告瞭 order,因此在 Post 上呼叫 order 將作為附加訂單新增,而不是覆蓋預設值。

Post.order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."created_at" DESC, "posts"."updated_at" DESC

這可能不是你想要的行為; 你可以通過首先從範圍中排除 order 來覆蓋它

Post.except(:order).order(updated_at: :desc)
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' ORDER BY "posts"."updated_at" DESC

default_scope 和模型初始化

與任何其他 ActiveRecord::Relation 一樣,default_scope 將改變從其初始化的模型的預設狀態。

在上面的例子中,Post 預設設定了 where(published: true),因此 Post 的新模型也會設定它。

Post.new # => <Post published: true>

unscoped

default_scope 名義上可以通過首先呼叫 unscoped 來清除,但這也有副作用。以 STI 模型為例:

class Post < Document
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

預設情況下,對 Post 的查詢將限定為包含'Post'type 列。但是 unscoped 將與你自己的 default_scope 一起清除它,所以如果你使用 unscoped,你必須記住也要考慮到這一點。

Post.unscoped.where(type: 'Post').order(updated_at: :desc)

unscoped 和模型關聯

考慮一下 PostUser 之間的關係

class Post < ApplicationRecord
  belongs_to :user
  default_scope ->{ where(published: true).order(created_at: :desc) }
end

class User < ApplicationRecord
  has_many :posts
end

通過獲取個人 User,你可以看到與之相關的帖子:

user = User.find(1)
user.posts
SELECT "posts".* FROM "posts" WHERE "posts"."published" = 't' AND "posts"."user_id" = ? ORDER BY "posts"."created_at" DESC [["user_id", 1]]

但是你想從 posts 關係中清除 default_scope,所以你使用 unscoped

user.posts.unscoped
SELECT "posts".* FROM "posts"

這消除了 user_id 條件以及 default_scope

default_scope 的一個示例用例

儘管如此,有些情況下使用 default_scope 是合理的。

考慮一個多租戶系統,其中多個子域由同一個應用程式提供,但具有獨立的資料。實現這種隔離的一種方法是通過 default_scope。其他情況下的缺點在這裡成為上升空間。

class ApplicationRecord < ActiveRecord::Base
  def self.inherited(subclass)
    super

    return unless subclass.superclass == self
    return unless subclass.column_names.include? 'tenant_id'

    subclass.class_eval do
      default_scope ->{ where(tenant_id: Tenant.current_id) }
    end
  end
end

你需要做的就是在請求的早期將 Tenant.current_id 設定為某個內容,任何包含 tenant_id 的表都將自動成為範圍,而無需任何其他程式碼。例項化記錄將自動繼承其建立的租戶 ID。

關於這個用例的重要一點是,每個請求都設定了一次範圍,並且它不會改變。這裡你需要 unscoped 的唯一情況是特殊情況,例如在請求範圍之外執行的後臺工作者。