注意默认范围

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 的唯一情况是特殊情况,例如在请求范围之外运行的后台工作者。