我的班级有以下范围,称为
Collection
:
scope :with_missing_coins, joins(:coins).where("coins.is_missing = ?", true)
我可以运行
Collection.with_missing_coins.count
并得到结果——效果很好!
目前,如果我想获得不丢失硬币的收藏,我会添加另一个范围:
scope :without_missing_coins, joins(:coins).where("coins.is_missing = ?", false)
我发现自己写了很多这些“相反”的范围。是否可以在不牺牲可读性或诉诸 lambda/方法(采用
true
或 false
作为参数)的情况下获得与作用域相反的结果?
类似这样的:
Collection.!with_missing_coins
在 Rails 4.2 中,您可以执行以下操作:
scope :original, -> { ... original scope definition ... }
scope :not_original, -> { where.not(id: original) }
它将使用子查询。
我不会使用单个范围,而是两个:
scope :with_missing_coins, joins(:coins).where("coins.is_missing = ?", true)
scope :without_missing_coins, joins(:coins).where("coins.is_missing = ?", false)
这样,当使用这些范围时,就可以清楚地知道发生了什么。根据numbers1311407的暗示,目前还不清楚
false
对with_missing_coins
的论证正在做什么。
我们应该尝试编写尽可能清晰的代码,如果这意味着偶尔不再热衷于 DRY,那就这样吧。
虽然我不认为诉诸 lambda 方法是一个问题,但范围本身并没有“逆转”。
scope :missing_coins, lambda {|status|
joins(:coins).where("coins.is_missing = ?", status)
}
# you could still implement your other scopes, but using the first
scope :with_missing_coins, lambda { missing_coins(true) }
scope :without_missing_coins, lambda { missing_coins(false) }
然后:
Collection.with_missing_coins
Collection.without_missing_coins
您可以通过编程方式执行此操作,无需子查询:
scope :my_scope, ...
Entity.where.not(Entity.my_scope.where_values_hash)
另请参阅:https://makandracards.com/makandra/486959-how-to-negate-scope-conditions-in-rails
请注意,这仅适用于 AR 查询,不适用于 SQL 查询。 例如
where('updated_at < ?', 1.hour.ago)
不会被否定。
TL;博士
Collection.with_missing_coins.invert_where
更多详情
invert_where
它允许您反转整个 where 子句,而不是手动应用条件
class User < ApplicationRecord
scope :active, -> { where(accepted: true, locked: false) }
end
User.where(accepted: true)
# SELECT * FROM users WHERE accepted = TRUE
User.where(accepted: true).invert_where
# SELECT * FROM users WHERE accepted != TRUE
User.active
# SELECT * FROM users WHERE accepted = TRUE AND locked = FALSE
User.active.invert_where
# SELECT * FROM users WHERE NOT (accepted = TRUE AND locked = FALSE)
小心,比较这些变体
User.where(role: "admin").active.invert_where
# SELECT * FROM users WHERE NOT (role = 'admin' AND accepted = TRUE AND locked = FALSE)
User.active.invert_where.where(role: "admin")
# SELECT * FROM users WHERE NOT (accepted = TRUE AND locked = FALSE) AND role = 'admin'
看看这个
User.where(accepted: true).invert_where.where(locked: false).invert_where
# SELECT * FROM users WHERE NOT (accepted != TRUE AND locked = FALSE)
invert_where
反转其之前的所有 where 条件。因此,最好避免在作用域中使用此方法,因为它可能会导致意外结果
最好明确地使用此方法,并且仅在需要反转所有先前条件之后立即在 where 链的开头
invert_where
:
scope :with_missing_coins, -> { joins(:coins).where(coins: { is_missing: true }) }
scope :without_missing_coins, -> { with_missing_coins.invert_where }
但是;
Collection.with_something.without_missing_coins
也会反转 with_something
,返回“没有任何内容”的行。您需要注意范围和条件的顺序:Collection.without_missing_coins.with_something
。!= true
,而不是= false
),否定范围也将包含空值;除非该字段定义为 null: false
。因此,明确地编写它们可能会更好、更清晰。如果你想合并它们,也许你可以使用另一个参数化范围:
scope :missing_coins, ->(is_missing) { joins(:coins).where(coins: { is_missing: is_missing }) }
scope :with_missing_coins, -> { missing_coins(true) }
scope :without_missing_coins, -> { missing_coins(false) }
这可能可行,没有进行太多测试。使用rails 5我猜rails 3有where_values方法而不是where_clause。
scope :not, ->(scope_name) do
query = self
send(scope_name).joins_values.each do |join|
query = query.joins(join)
end
query.where((send(scope_name).
where_clause.send(:predicates).reduce(:and).not))
end
使用方法
Model.not(:scope_name)
@bill-lipa 的答案很好,但当你想要的范围涉及关联或附件时要小心。
如果我们有以下范围来选择所有附有简历的用户:
scope :with_resume, -> { joins(:resume_attachment) }
如果
with_resume
为空,以下否定将导致异常:
scope :without_resume, -> { where.not(id: with_resume) }
#=> users.without_resume
#=> ActiveRecord::StatementInvalid (PG::InvalidColumnReference: ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
因此,您需要先检查以获得您想要的结果:
scope :without_resume, -> { with_resume.any? ? where.not(id: with_resume) : where.not(id: []) }
是的,在 Rails 中否定作用域是绝对有可能的! Rails 提供了诸如 where.not 之类的工具来从查询中排除特定记录。通过定义自定义否定范围,您可以干净高效地检索不满足某些条件的记录,从而使代码更具可读性和可重用性。 例如,如果您有一个带有状态属性的 User 模型,并且想要过滤掉被禁止的用户,您可以创建一个 not_banned 范围,使用 where.not 来排除它们:
class User < ApplicationRecord
scope :not_banned, -> { where.not(status: 'banned') }
end
通过调用 User.not_banned,您将仅获得非禁止用户。否定范围对于复杂应用程序中的频繁排除特别有用。有关在 Rails 中使用自定义作用域和创建否定作用域的更多信息,请查看此详细指南:Rails 模型中的自定义作用域与 ActiveRecord 否定作用域。
更新。现在 Rails 6 添加了方便且漂亮的负枚举方法。
# All inactive devices
# Before
Device.where.not(status: :active)
#After
Device.not_active
博客文章 这里