Is there a way to invert an ActiveRecord::Relation query?
Let's say we have the following:
irb> Post.where(:hidden => true).to_sql
=&开发者_C百科gt; "SELECT `posts`.* FROM `posts` WHERE posts.hidden = 1"
Could we somehow get an inverted SQL query out of it?
What I am looking for, should probably look like this:
irb> Post.where(:hidden => true).invert.to_sql
=> "SELECT `posts`.* FROM `posts` WHERE NOT (posts.hidden = 1)"
With a different syntax, yes. Example:
posts = Post.scoped.table # or Arel::Table.new("posts")
posts.where(posts[:hidden].eq(true).not).to_sql
# => SELECT FROM `posts` WHERE NOT ((`posts`.`hidden` = 1))
In rails 4 there is not
suffix for this purpose:
Post.where.not(hidden: true).to_sql
# => SELECT FROM `posts` WHERE `posts`.`hidden` != 1
In rails 3 you can use squeel gem. It gives many usefull features. And with it you can write:
Post.where{ hidden != true }.to_sql
# => SELECT FROM `posts` WHERE `posts`.`hidden` != 1
We can take Zabba's answer further by passing the inverted query back into ActiveRecord:
table = Post.arel_table
query = table[:hidden].eq(true).not # the inverted query, still ARel
Post.where(query) # plug it back into ActiveRecord
This will return ActiveRecord objects, as you would normally expect.
invert_where (Rails 7+)
Starting from Rails 7, there is a new invert_where method.
According to the docs, it:
Allows you to invert an entire where clause instead of manually applying conditions.
class User
scope :active, -> { where(accepted: true, locked: false) }
end
User.where(accepted: true)
# WHERE `accepted` = 1
User.where(accepted: true).invert_where
# WHERE `accepted` != 1
User.active
# WHERE `accepted` = 1 AND `locked` = 0
User.active.invert_where
# WHERE NOT (`accepted` = 1 AND `locked` = 0)
Be careful because this inverts all conditions before invert_where call.
class User
scope :active, -> { where(accepted: true, locked: false) }
scope :inactive, -> { active.invert_where } # Do not attempt it
end
# It also inverts `where(role: 'admin')` unexpectedly.
User.where(role: 'admin').inactive
# WHERE NOT (`role` = 'admin' AND `accepted` = 1 AND `locked` = 0)
Sources:
- invert_where from official Rails API docs.
- Link to the PR.
- BigBinary blog article about invert_where method.
What I do when I'm looking for records with a "not true" condition (eg, false or nil) is:
Post.where(["(hidden IS NULL) OR (hidden = ?)", false])
Finally we have the method invert_where
with Rails 7.
irb> Post.where(:hidden => true).invert_where.to_sql
"SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"hidden\" != 1"
Please check this commit reference for more details.
精彩评论