Rails Scope返回all而不是nil

我遇到一个奇怪的问题,创建一个范围并使用第first查找器。 看起来好像在作用域中使用first作为查询的一部分将使得如果没有找到结果则返回所有结果。 如果找到任何结果,它将正确返回第一个结果。

我已经设置了一个非常简单的测试来certificate这一点:

 class Activity::MediaGroup  { where('1 = 0').first } scope :test_pass, -> { where('1 = 1').first } end 

注意这个测试,我已经设置了匹配记录的条件。 实际上,我根据实际情况进行查询,并获得相同的奇怪行为。

以下是失败范围的结果。 正如您所看到的,它会生成正确的查询,该查询没有结果,因此它会查询所有匹配的记录并返回:

 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 [#, #, #, #]> 

另一个范围按预期运行:

 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 => # 

如果我在范围之外执行相同的逻辑,我会得到预期的结果:

 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中的一个错误,除非有一些我不知道的未知行为期望。

这不是一个错误或怪异,经过一些研究我发现它是故意设计的

首先,

  1. scope返回ActiveRecord::Relation

  2. 如果没有记录,则编程返回所有记录,这些记录又是ActiveRecord::Relation而不是nil

这背后的想法是使范围可链接 (即) scopeclass methods之间的关键区别之一

例:

让我们使用以下场景:用户将能够按状态过滤post,按最新更新的顺序排序。 很简单,让我们为此编写范围:

 class Post < ActiveRecord::Base scope :by_status, -> status { where(status: status) } scope :recent, -> { order("posts.updated_at DESC") } end 

我们可以像这样自由地打电话给他们:

 Post.by_status('published').recent # SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' # ORDER BY posts.updated_at DESC 

或者使用用户提供的参数:

 Post.by_status(params[:status]).recent # SELECT "posts".* FROM "posts" WHERE "posts"."status" = 'published' # ORDER BY posts.updated_at DESC 

到现在为止还挺好。 现在让我们将它们移动到类方法,只是为了比较:

 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或空白会发生什么?

 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,我认为我们不想允许这些查询,是吗? 使用范围,我们可以通过向我们的范围添加状态条件来轻松解决这个问题:

 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 

真棒。 现在让我们尝试用我们心爱的类方法做同样的事情:

 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 

并且:炸弹: 区别在于范围总是返回一个关系,而我们的简单类方法实现则不会。 类方法应该是这样的:

 def self.by_status(status) if status.present? where(status: status) else all end end 

请注意,我将返回所有nil / blank case,它在Rails 4中返回一个关系(它之前返回了数据库中的项目数组)。 在Rails 3.2.x中,您应该使用作用域。 然后我们去:

 Post.by_status('').recent # SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC 

所以这里的建议是:永远不要从类应该像作用域一样的类方法返回nil,否则你就会破坏作用域隐含的可链接性条件,它总是返回一个关系。

长话短说:

无论如何,范围旨在返回ActiveRecord::Relation以使其可链接。 如果您期望firstlastfind结果,则应使用class methods

资料来源: http : //blog.plataformatec.com.br/2013/02/active-record-scopes-vs-class-methods/