Rails Scope返回all而不是nil

时间:2014-01-06 03:48:36

标签: ruby-on-rails activerecord scope

我遇到了一个奇怪的问题,即创建一个范围并使用first查找程序。似乎在作用域中使用first作为查询的一部分将使得如果找不到结果则返回所有结果。如果找到任何结果,它将正确返回第一个结果。

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

class Activity::MediaGroup < ActiveRecord::Base
  scope :test_fail, -> { 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 [#<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>

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

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

2 个答案:

答案 0 :(得分:55)

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

首先,

  1. scope会返回ActiveRecord::Relation

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

  3. 这背后的想法是使范围可链接(即)scopeclass methods

    之间的关键差异之一

    示例:

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

    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/

答案 1 :(得分:0)

您可以使用limit代替first,因为-

当找不到数据时,first返回nilfirst(<number>)返回不可链接对象的数组。

limit返回ActiveRecord::Relation对象。

此帖子中的更多详细信息- https://sagarjunnarkar.github.io/blogs/2019/09/15/activerecord-scope/