Rails Scope返回all而不是nil

Rya*_*yan 30 activerecord scope ruby-on-rails

我遇到了一个奇怪的问题,即创建一个范围并使用firstfinder.似乎first在作用域中使用作为查询的一部分将使其在未找到结果的情况下返回所有结果.如果找到任何结果,它将正确返回第一个结果.

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

class Activity::MediaGroup < ActiveRecord::Base
  scope :test_fail, -> { where('1 = 0').first }
  scope :test_pass, -> { where('1 = 1').first }
end
Run Code Online (Sandbox Code Playgroud)

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

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

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>]>
Run Code Online (Sandbox Code Playgroud)

另一个范围按预期运行:

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>
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

我在这里错过了什么吗?这似乎是Rails/ActiveRecord/Scopes中的一个错误,除非有一些我不知道的未知行为期望.

Siv*_*iva 56

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

首先,

  1. scope返回的ActiveRecord::Relation

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

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

例:

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

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

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

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

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

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

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

class Post < ActiveRecord::Base
  def self.by_status(status)
    where(status: status)
  end

  def self.recent
    order("posts.updated_at DESC")
  end
end
Run Code Online (Sandbox Code Playgroud)

除了使用一些额外的线路,没有大的改进.但是现在如果: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
Run Code Online (Sandbox Code Playgroud)

Oooops,我认为我们不想允许这些查询,是吗?使用范围,我们可以通过在我们的范围中添加状态条件来轻松解决这个问题:

scope :by_status, -> status { where(status: status) if status.present? }
Run Code Online (Sandbox Code Playgroud)

我们去:

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
Run Code Online (Sandbox Code Playgroud)

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

class Post < ActiveRecord::Base
  def self.by_status(status)
    where(status: status) if status.present?
  end
end
Run Code Online (Sandbox Code Playgroud)

运行这个:

Post.by_status('').recent
NoMethodError: undefined method `recent' for nil:NilClass
Run Code Online (Sandbox Code Playgroud)

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

def self.by_status(status)
  if status.present?
    where(status: status)
  else
    all
  end
end
Run Code Online (Sandbox Code Playgroud)

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

Post.by_status('').recent
# SELECT "posts".* FROM "posts" ORDER BY posts.updated_at DESC
Run Code Online (Sandbox Code Playgroud)

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

长话短说:

无论如何,范围都旨在ActiveRecord::Relation使其可以链接.如果您期望first,last或者 find您应该使用结果class methods

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