Rails 中是否可以否定作用域?

问题描述 投票:0回答:10

我的班级有以下范围,称为

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
ruby-on-rails ruby named-scope
10个回答
83
投票

在 Rails 4.2 中,您可以执行以下操作:

scope :original, -> { ... original scope definition ... }
scope :not_original, -> { where.not(id: original) }

它将使用子查询。


15
投票

我不会使用单个范围,而是两个:

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,那就这样吧。


12
投票

虽然我不认为诉诸 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

5
投票

您可以通过编程方式执行此操作,无需子查询:

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)
不会被否定。


5
投票

TL;博士

Collection.with_missing_coins.invert_where

更多详情

Rails 7 推出

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 链的开头


3
投票

使用 Rails 7,您可以使用

invert_where
:

scope :with_missing_coins, -> { joins(:coins).where(coins: { is_missing: true }) }
scope :without_missing_coins, -> { with_missing_coins.invert_where }

但是;

  1. 它将反转它之前的所有内容,因此
    Collection.with_something.without_missing_coins
    也会反转
    with_something
    ,返回“没有任何内容”的行。您需要注意范围和条件的顺序:
    Collection.without_missing_coins.with_something
  2. 由于它反转条件而不是参数(即将其转换为
    != 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) }

2
投票

这可能可行,没有进行太多测试。使用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)


0
投票

@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: []) }

0
投票

是的,在 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 否定作用域。


-2
投票

更新。现在 Rails 6 添加了方便且漂亮的负枚举方法。

# All inactive devices
# Before
Device.where.not(status: :active)
#After 
Device.not_active

博客文章 这里

© www.soinside.com 2019 - 2024. All rights reserved.