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/