Rails 7 adds #excluding to ActiveRecord::Relation


Rails 7 adds #excluding method for an ActiveRecord::Relation to exclude specific record (or collection of records) from the resulting relation.

We come across cases in our Rails application, where we need to exclude a few set of records when fetching data. In such cases, we can use the excluding method to filter out unwanted records.

Before

Rails 4.0 added where.not to exclude a record or set of records from queries. We pass our exclude conditions in the not clause.

To ignore a particular post with a specific id we can use the below query.

Post.where.not(id: 1)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1

To ignore the collection of records we can pass our condition in the not clause as below.

Post.where.not(id: [1, 2])
# SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2).

where.not can also be used on associations.

Let’s say we have Post and Comment models. We want to ignore few comments on a particular post. We chain where.not clause to post.comments query as shown below.

post = Post.find(1)

post.comments.where.not(id: 1)
# SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" != 1

post.comments.where.not(id: [1, 2])
# SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" NOT IN (1, 2)

After

Rails 7 added #excluding method which serves the same purpose as where.not. But unlike where.not we pass objects to the excluding method instead of object attributes.

To ignore a particular post with a specific id we can use the below query.

post = Post.find(1)

Post.excluding(post)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1

To exclude the collection of posts we can pass its objects to the excluding method as below

post_one = Post.find(1)
post_two = Post.find(2)

Post.all.excluding(post_one, post_two)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2)

Similar to where.not we can use excluding on associations as shown below

post_one = Post.find(1)

# And on associations
comment_one = Comment.find_by(id: 1, post: post_one)
comment_two = Comment.find_by(id: 2, post: post_one)

post_one.comments.excluding(comment_one)
# SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" != 1

post_one.comments.excluding(comment_one, comment_two)
# SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" NOT IN (1, 2)

When no condition is passed to the excluding clause it returns all records.

# same as Post.all
Post.all.excluding
# SELECT "posts".* FROM "posts"

NOTE

An ArgumentError 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.

Post.excluding(1)
ArgumentError (You must only pass a single or collection of Post objects to #excluding.)

user = User.find(1)

Post.excluding(user)
ArgumentError (You must only pass a single or collection of Post objects to #excluding.)

We can also use the alias method #without in-place of #excluding.

post = Post.find(1)

Post.without(post)
# SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1