Ruby on Rails ActiveRecord范围与类方法

Rails在内部将范围转换为类方法,那么为什么我们不能自己使用类方法而不是使用范围。

从精美指南 :

14范围
[…]
要定义一个简单的作用域,我们在类中使用scope方法,在调用此作用域时传递我们想要运行的查询:

 class Article < ActiveRecord::Base scope :published, -> { where(published: true) } end 

这与定义类方法完全相同,您使用的是个人偏好:

 class Article < ActiveRecord::Base def self.published where(published: true) end end 

特别注意:

这与定义类方法完全相同,您使用的是个人偏好

再进一步 (Rails3指南在BTW中说同样的事情):

14.1传递参数
[...]
使用类方法是接受范围参数的首选方法。

所以你使用它是一个偏好的问题,甚至建议你对带参数的作用域使用类方法。

使用scope主要是一个符号问题。 如果你说scope :whatever你明确地说whatever是一个查询构建者; 如果你说def self.whatever那么你并没有暗示任何方法的意图,你只是定义一些类方法可能会或可能不像一个范围。

当然, 14.1通过建议在范围采用参数时不使用scope弄清楚这种符号的区别。 还要记住,在Rails3中你可以说:

 scope :published, where(published: true) 

所以一个无争议的范围在视觉上“干净”和简洁,但添加一个lambda来处理参数将使它看起来更混乱:

 scope :pancakes, ->(x) { where(things: x) } 

但是Rails4甚至想要无争议的范围内的lambdas,这种区别现在更没意义了。

我怀疑此时的差异是历史性的。 范围在之前的时代可能是一些特殊的东西,但在Rails3时代变成了普通的旧类方法,以减少重复并更好地与Rails3附带的新查询接口进行网格化。


因此,如果您愿意,可以跳过scope并直接进入课程方法。 当您的范围需要参数时,甚至鼓励您这样做。

Scopes只是class methods. 内部活动记录将范围转换为类方法。

“他们之间没有区别”或“这是品味的问题”。 我倾向于同意这两个句子,但我想表明两者之间存在一些细微差别。 这篇博客很好地解释了这种差异。

如果它只是类方法的语法糖,我为什么要使用范围?“ 所以这里有一些有趣的例子供你思考。

示波器总是可链接的=>••••••••••••••••••••••••••••••••••••••••••让我们使用以下场景:用户将能够按状态过滤post,按最新更新的顺序排序。 很简单,让我们为此编写范围:

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

  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? } There we go: 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 Running this: Post.by_status('').recent NoMethodError: undefined method `recent' for nil:NilClass And . 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 case,它在Rails 4中返回一个关系(它之前返回了数据库中的项目数组)。 在Rails 3.2.x中,您应该使用作用域。 然后我们去:

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

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

范围是可扩展的=>•••••••••••••••••••••••••••让我们作为下一个示例获取分页,我将使用kaminarigem作为基础。 在对集合进行分页时,您需要做的最重要的事情是告诉您要获取哪个页面:

  Post.page(2) After doing that you might want to say how many records per page you want: Post.page(2).per(15) And you may to know the total number of pages, or whether you are in the first or last page: posts = Post.page(2) posts.total_pages # => 2 posts.first_page? # => false posts.last_page? # => true 

当我们按此顺序调用事物时,这一切都有意义,但是在没有分页的集合中调用这些方法没有任何意义,是吗? 编写范围时,可以添加仅在对象中可用的特定扩展(如果调用该范围)。 对于kaminari,它只将页面范围添加到Active Record模型,并依赖范围扩展function在调用页面时添加所有其他function。 从概念上讲,代码看起来像这样:

  scope :page, -> num { # some limit + offset logic here for pagination } do def per(num) # more logic here end def total_pages # some more here end def first_page? # and a bit more end def last_page? # and so on end end 

范围扩展是我们工具链中强大而灵活的技术。 但是,当然,我们总是可以疯狂地通过类方法获得所有这些:

  def self.page(num) scope = # some limit + offset logic here for pagination scope.extend PaginationExtensions scope end module PaginationExtensions def per(num) # more logic here end def total_pages # some more here end def first_page? # and a bit more end def last_page? # and so on end end 

它比使用范围更冗长,但它产生相同的结果。 这里的建议是:选择哪种方法更适合你,但要确保在重新发明轮子之前知道框架提供了什么。