Rails 中是否可以否定范围?

发布于 2024-11-29 11:20:50 字数 548 浏览 1 评论 0原文

我的类名为 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/方法(采用 truefalse 作为参数)的情况下获得作用域的相反结果?

像这样的东西:

Collection.!with_missing_coins

I have the following scope for my class called Collection:

scope :with_missing_coins, joins(:coins).where("coins.is_missing = ?", true)

I can run Collection.with_missing_coins.count and get a result back -- it works great!
Currently, if I want to get collections without missing coins, I add another scope:

scope :without_missing_coins, joins(:coins).where("coins.is_missing = ?", false)

I find myself writing a lot of these "opposite" scopes. Is it possible to get the opposite of a scope without sacrificing readability or resorting to a lambda/method (that takes true or false as a parameter)?

Something like this:

Collection.!with_missing_coins

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

原来分手还会想你 2024-12-06 11:20:50

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

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

它将使用子查询。

In Rails 4.2, you can do:

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

It'll use a subquery.

转身以后 2024-12-06 11:20:50

我不会为此使用单个作用域,而是使用两个作用域:

scope :with_missing_coins, joins(:coins).where("coins.is_missing = ?", true)
scope :without_missing_coins, joins(:coins).where("coins.is_missing = ?", false)

这样,当使用这些作用域时,就可以清楚地了解发生的情况。根据 Numbers1311407 的建议,目前还不清楚 with_missing_coinsfalse 参数在做什么。

我们应该尝试编写尽可能清晰的代码,如果这意味着偶尔不再那么热衷于 DRY,那就这样吧。

I wouldn't use a single scope for this, but two:

scope :with_missing_coins, joins(:coins).where("coins.is_missing = ?", true)
scope :without_missing_coins, joins(:coins).where("coins.is_missing = ?", false)

That way, when these scopes are used then it's explicit what's happening. With what numbers1311407 suggests, it is not immediately clear what the false argument to with_missing_coins is doing.

We should try to write code as clear as possible and if that means being less of a zealot about DRY once in while then so be it.

轮廓§ 2024-12-06 11:20:50

尽管我不认为诉诸 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

There's no "reversal" of a scope per se, although I don't think resorting to a lambda method is a problem.

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) }

then:

Collection.with_missing_coins
Collection.without_missing_coins
只怪假的太真实 2024-12-06 11:20:50

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

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

You can do this programmatically, without a subquery:

scope :my_scope, ...

Entity.where.not(Entity.my_scope.where_values_hash)

See also: https://makandracards.com/makandra/486959-how-to-negate-scope-conditions-in-rails

Note this only works with AR queries and does not work with SQL queries. E.g. where('updated_at < ?', 1.hour.ago) would not be negated.

百善笑为先 2024-12-06 11:20:50

TL;DR

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

TL;DR

Collection.with_missing_coins.invert_where

More details

Rails 7 introduced invert_where

It allows you to invert an entire where clause instead of manually applying conditions

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)

Be careful, compare these variants

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'

And look at this

User.where(accepted: true).invert_where.where(locked: false).invert_where
# SELECT * FROM users WHERE NOT (accepted != TRUE AND locked = FALSE)

invert_where inverts all where conditions before it. Therefore it's better to avoid using this method in scopes because it could lead to unexpected results

Much better to use this method explicitly and only in the beginning of where chain immediately after it is necessary to invert all the previous conditions

神妖 2024-12-06 11:20:50

对于 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 值;除非该字段定义为 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) }

With Rails 7, you can use invert_where:

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

However;

  1. It will invert everything before it, so Collection.with_something.without_missing_coins will invert with_something too, returning rows "without something". You'll need to watch out for the order of scopes and conditions: Collection.without_missing_coins.with_something.
  2. Since it inverts the condition and not the argument (ie. it converts it to != true, rather than = false), the negated scope will include null values too; unless the field is defined as null: false.

So it's probably better and clearer to write them explicitly. If you want to merge them, maybe you can use another parameterized scope:

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) }
南风起 2024-12-06 11:20:50

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

this might just work, did not test it much. uses rails 5 I guess rails 3 has where_values method instead of 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

usage

Model.not(:scope_name)

じ违心 2024-12-06 11:20:50

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

The answer by @bill-lipa is good, but beware when the scopes you want involve associations or attachments.

If we have the following scope to select all users with a resume attached:

scope :with_resume, -> { joins(:resume_attachment) } 

The following negation will cause an exception if with_resume is empty:

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

Therefore you'll need to check first to get the results you want:

scope :without_resume, -> { with_resume.any? ? where.not(id: with_resume) : where.not(id: []) }
机场等船 2024-12-06 11:20:50

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

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

博客文章
此处

Update . Now Rails 6 adds convenient and nifty negative enum methods.

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

Blog post
here

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文