I'm running into a strange issue creating a scope and using the first
finder. It seems as though using first
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.
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
andclass 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:
And we can call them freely like this:
Or with a user provided param:
So far, so good. Now lets move them to class methods, just for the sake of comparing:
Besides using a few extra lines, no big improvements. But now what happens if the :status parameter is nil or blank?
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:
There we go:
Awesome. Now lets try to do the same with our beloved class method:
Running this:
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:
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:
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 expectingfirst
,last
orfind
results you should useclass methods
Source: http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/