Rails Scope returns all instead of nil
This is not a bug or weirdness, after some research i've found its designed on purpose.
First of all,
The
scope
returns anActiveRecord::Relation
If there are zero records its programmed to return all records which is again an
ActiveRecord::Relation
instead ofnil
The idea behind this is to make scopes chainable (i.e) one of the key difference between scope
and class methods
Example:
Lets use the following scenario: users will be able to filter posts by statuses, ordering by most recent updated ones. Simple enough, lets write scopes for that:
class Post < ActiveRecord::Base
scope :by_status, -> status { where(status: status) }
scope :recent, -> { order("posts.updated_at DESC") }
end
And we can call them freely like this:
Post.by_status('published').recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published'
# ORDER BY posts.updated_at DESC
Or with a user provided param:
Post.by_status(params[:status]).recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published'
# ORDER BY posts.updated_at DESC
So far, so good. Now lets move them to class methods, just for the sake of comparing:
class Post < ActiveRecord::Base
def self.by_status(status)
where(status: status)
end
def self.recent
order("posts.updated_at DESC")
end
end
Besides using a few extra lines, no big improvements. But now what happens if the :status parameter is nil or blank?
Post.by_status(nil).recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" IS NULL
# ORDER BY posts.updated_at DESC
Post.by_status('').recent
# SELECT "posts".* FROM "posts" WHERE "posts"."status" = ''
# ORDER BY posts.updated_at DESC
Oooops, I don’t think we wanted to allow these queries, did we? With scopes, we can easily fix that by adding a presence condition to our scope:
scope :by_status, -> status { where(status: status) if status.present? }
There we go:
Post.by_status(nil).recent
# SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC
Post.by_status('').recent
# SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC
Awesome. Now lets try to do the same with our beloved class method:
class Post < ActiveRecord::Base
def self.by_status(status)
where(status: status) if status.present?
end
end
Running this:
Post.by_status('').recent
NoMethodError: undefined method `recent' for nil:NilClass
And :bomb:. The difference is that a scope will always return a relation, whereas our simple class method implementation will not. The class method should look like this instead:
def self.by_status(status)
if status.present?
where(status: status)
else
all
end
end
Notice that I’m returning all for the nil/blank case, which in Rails 4 returns a relation (it previously returned the Array of items from the database). In Rails 3.2.x, you should use scoped there instead. And there we go:
Post.by_status('').recent
# SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC
So the advice here is: never return nil from a class method that should work like a scope, otherwise you’re breaking the chainability condition implied by scopes, that always return a relation.
Long Story Short:
No matter what, scopes are intended to return ActiveRecord::Relation
to make it chainable. If you are expecting first
, last
or find
results you should use class methods
Source: http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/
Ryan
Updated on June 05, 2022Comments
-
Ryan almost 2 years
I'm running into a strange issue creating a scope and using the
first
finder. It seems as though usingfirst
as part of the query in a scope will make it return all results if no results are found. If any results are found, it will correctly return the first result.I have setup a very simple test to demonstrate this:
class Activity::MediaGroup < ActiveRecord::Base scope :test_fail, -> { where('1 = 0').first } scope :test_pass, -> { where('1 = 1').first } end
Note for this test, I have set where conditions to match records or not. In reality, I am querying based on real conditions, and getting the same strange behavior.
Here are the results from the failing scope. As you can see, it makes the correct query, which has no results, so it then queries for all matching records and returns that instead:
irb(main):001:0> Activity::MediaGroup.test_fail Activity::MediaGroup Load (0.0ms) SELECT "activity_media_groups".* FROM "activity_media_groups" WHERE (1 = 0) ORDER BY "activity_media_groups"."id" ASC LIMIT 1 Activity::MediaGroup Load (0.0ms) SELECT "activity_media_groups".* FROM "activity_media_groups" => #<ActiveRecord::Relation [#<Activity::MediaGroup id: 1, created_at: "2014-01-06 01:00:06", updated_at: "2014-01-06 01:00:06", user_id: 1>, #<Activity::MediaGroup id: 2, created_at: "2014-01-06 01:11:06", updated_at: "2014-01-06 01:11:06", user_id: 1>, #<Activity::MediaGroup id: 3, created_at: "2014-01-06 01:26:41", updated_at: "2014-01-06 01:26:41", user_id: 1>, #<Activity::MediaGroup id: 4, created_at: "2014-01-06 01:28:58", updated_at: "2014-01-06 01:28:58", user_id: 1>]>
The other scope operates as expected:
irb(main):002:0> Activity::MediaGroup.test_pass Activity::MediaGroup Load (1.0ms) SELECT "activity_media_groups".* FROM "activity_media_groups" WHERE (1 = 1) ORDER BY "activity_media_groups"."id" ASC LIMIT 1 => #<Activity::MediaGroup id: 1, created_at: "2014-01-06 01:00:06", updated_at: "2014-01-06 01:00:06", user_id: 1>
If I perform this same logic outside of a scope, I get the expected results:
irb(main):003:0> Activity::MediaGroup.where('1=0').first Activity::MediaGroup Load (0.0ms) SELECT "activity_media_groups".* FROM "activity_media_groups" WHERE (1=0) ORDER BY "activity_media_groups"."id" ASC LIMIT 1 => nil
Am I missing something here? This seems like a bug in Rails/ActiveRecord/Scopes to me unless there is some unknown behavior expectations I am unaware of.
-
Ryan over 10 yearsUsing limit, even with a limit of 1, returns an Active Record Relation though, whereas first returns an instance of the model. So, the functionality is different, and in my case that matters. Is there any other way to get one model, if it exists, from the scope, and not a relation?
-
Siva over 10 years@Ryan is there any reason for you to not use class methods ?
-
Ryan over 10 yearsThanks for the info, I see what you're saying... and I see the reasoning. Although I still don't think I fully agree that a scope should return something that is explicitly not true. Can an active record relation not contain zero records? I guess you just have to be very conscious of not using scopes if using functions like first, last, or find.
-
Yo Ludke almost 10 yearsPerfect answer I had the opposite question of the OP but it still helped me!
-
Patelify over 9 yearsGood answer. When working with custom scopes, I found returning nil accomplished the exact goal that I thought returning self from within a custom scope would accomplish. Returning self caused the previous scope chaining to be ignored. Returning nil allowed for the previous chaining to be used. Kudos!
-
cavpollo about 8 yearsThanks! Super Long Story Short: scopes + where doesn't return nil. Ever.
-
Tilo about 7 yearsbut you can return
scoped
instead ofall
-
Dorian over 2 years
limit
doesn't return a model but a collection/active record relation