Rails Scope返回all而不是nil [英] Rails Scope returns all instead of nil
问题描述
我遇到了一个奇怪的问题,即创建一个示波器并使用 first
查找程序。似乎在范围中使用 first
作为查询的一部分会使它返回所有结果(如果未找到结果)。如果找到任何结果,它将正确返回第一个结果。
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>]>
其他范围按预期运行:
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
我在这里错过了什么吗?对我来说,这似乎是Rails / ActiveRecord / Scopes中的错误,除非我没有意识到某些未知的行为期望。
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.
首先,
-
范围
返回ActiveRecord :: Relation
如果记录为零,则将其编程为返回所有记录
,而这又是 ActiveRecord: :关系
而不是 nil
If there are zero records its programmed to return all records
which is again an ActiveRecord::Relation
instead of nil
其背后的想法是使范围可链接(即)是范围
和类方法
The idea behind this is to make scopes chainable (i.e) one of the key difference between scope
and class methods
示例:
让我们使用以下情况:用户将能够按状态,最新更新的顺序对帖子进行过滤。足够简单,让我们为它写范围:
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
除了使用一些额外的行,没有太大的改进。但是,如果:status参数为nil或空白,会发生什么?
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? }
我们去了:
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
运行此:
Post.by_status('').recent
NoMethodError: undefined method `recent' for nil:NilClass
和:bomb:。区别在于,范围将始终返回关系,而我们的简单类方法实现则不会。类方法应该看起来像这样:
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
注意,我将返回所有nil / blank情况,在Rails 4中返回一个关系(它先前从数据库返回了Array of Items)。在Rails 3.2.x中,应该在此处使用范围。然后就可以了:
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
所以这里的建议是:永远不要从类中返回nil应该像作用域一样工作的方法,否则您将打破作用域所隐含的可链接性条件,该条件总是返回一个关系。
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.
长篇小说简短:
无论如何,作用域旨在返回 ActiveRecord :: Relation
以使其可链接。如果您期望第一
,最后
或查找
,应该使用类方法
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
来源:http://blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/
这篇关于Rails Scope返回all而不是nil的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!