1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00

Implemented ActiveRecord::Relation#excluding method.

This method excludes the specified record (or collection of records) from the resulting relation.

For example: `Post.excluding(post)`, `Post.excluding(post_one, post_two)`, and `post.comments.excluding(comment)`.

This is short-hand for `Post.where.not(id: post.id)` (for a single record) and `Post.where.not(id: [post_one.id, post_two.id])` (for a collection).
This commit is contained in:
Glen Crawford 2021-02-14 19:41:49 +11:00
parent 5585c375d1
commit 690fdbb2b9
4 changed files with 139 additions and 1 deletions

View file

@ -1,3 +1,25 @@
* Implemented `ActiveRecord::Relation#excluding` method.
This method excludes the specified record (or collection of records) from
the resulting relation:
```ruby
Post.excluding(post)
Post.excluding(post_one, post_two)
```
Also works on associations:
```ruby
post.comments.excluding(comment)
post.comments.excluding(comment_one, comment_two)
```
This is short-hand for `Post.where.not(id: post.id)` (for a single record)
and `Post.where.not(id: [post_one.id, post_two.id])` (for a collection).
*Glen Crawford*
* Skip optimised #exist? query when #include? is called on a relation
with a having clause

View file

@ -17,7 +17,7 @@ module ActiveRecord
:and, :or, :annotate, :optimizer_hints, :extending,
:having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate,
:pluck, :pick, :ids, :strict_loading
:pluck, :pick, :ids, :strict_loading, :excluding
].freeze # :nodoc:
delegate(*QUERYING_METHODS, to: :all)

View file

@ -1105,6 +1105,51 @@ module ActiveRecord
self
end
# Excludes the specified record (or collection of records) from the resulting
# relation. For example:
#
# Post.excluding(post)
# # SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1
#
# Post.excluding(post_one, post_two)
# # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2)
#
# This can also be called on associations. As with the above example, either
# a single record of collection thereof may be specified:
#
# post = Post.find(1)
# comment = Comment.find(2)
# post.comments.excluding(comment)
# # SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" != 2
#
# This is short-hand for <tt>.where.not(id: post.id)</tt> and <tt>.where.not(id: [post_one.id, post_two.id])</tt>.
#
# An <tt>ArgumentError</tt> will be raised if either no records are
# specified, or if any of the records in the collection (if a collection
# is passed in) are not instances of the same model that the relation is
# scoping.
def excluding(*records)
records.flatten!(1)
raise ArgumentError, "You must pass at least one #{klass.name} object to #excluding." if records.empty?
if records.any? { |record| !record.is_a?(klass) }
raise ArgumentError, "You must only pass a single or collection of #{klass.name} objects to #excluding."
end
spawn.excluding!(records)
end
def excluding!(records) # :nodoc:
# Treat single and multiple records differently in order to keep query
# clean in case of single record, ie, use != operator instead of NOT IN ().
if records.one?
where.not(primary_key => records.first.id)
else
where.not(primary_key => records)
end
end
# Returns the Arel object associated with the relation.
def arel(aliases = nil) # :nodoc:
@arel ||= build_arel(aliases)

View file

@ -0,0 +1,71 @@
# frozen_string_literal: true
require "cases/helper"
require "models/post"
require "models/comment"
class ExcludingTest < ActiveRecord::TestCase
fixtures :posts, :comments
def test_result_set_does_not_include_single_excluded_record
post = posts(:welcome)
assert_not_includes Post.excluding(post).to_a, post
end
def test_result_set_does_not_include_collection_of_excluded_records
post_welcome = posts(:welcome)
post_thinking = posts(:thinking)
relation = Post.excluding(post_welcome, post_thinking)
assert_not_includes relation.to_a, post_welcome
assert_not_includes relation.to_a, post_thinking
end
def test_result_set_through_association_does_not_include_single_excluded_record
post = posts(:welcome)
comment_greetings = comments(:greetings)
comment_more_greetings = comments(:more_greetings)
relation = post.comments.excluding(comment_greetings)
assert_not_includes relation.to_a, comment_greetings
assert_includes relation.to_a, comment_more_greetings
end
def test_result_set_through_association_does_not_include_collection_of_excluded_records
post = posts(:welcome)
comment_greetings = comments(:greetings)
comment_more_greetings = comments(:more_greetings)
relation = post.comments.excluding([comment_greetings, comment_more_greetings])
assert_not_includes relation.to_a, comment_greetings
assert_not_includes relation.to_a, comment_more_greetings
end
def test_raises_on_no_arguments
exception = assert_raises ArgumentError do
Post.excluding()
end
assert_equal "You must pass at least one Post object to #excluding.", exception.message
end
def test_raises_on_empty_collection_argument
exception = assert_raises ArgumentError do
Post.excluding([])
end
assert_equal "You must pass at least one Post object to #excluding.", exception.message
end
def test_raises_on_record_from_different_class
post = posts(:welcome)
comment = comments(:greetings)
exception = assert_raises ArgumentError do
Post.excluding(post, comment)
end
assert_equal "You must only pass a single or collection of Post objects to #excluding.", exception.message
end
end