Should You Use Scopes or Class Methods?

Scopes 可以帮我们从数据库中获取正确的数据:

# app/models/review.rb
class Review < ActiveRecord::Base
  scope :most_recent, -> (limit) { order("created_at desc").limit(limit) }
end

# app/models/homepage_controller.rb
@recent_reviews = Review.most_recent(5)

当然,我们也可以用类方法来达到同样的目的:

# app/models/review.rb
def self.most_recent(limit)
  order("created_at desc").limit(limit)
end

# app/models/homepage_controller.rb
@recent_reviews = Review.most_recent(5)

Scopes 其实就是类方法

在 Rails 内部 Active Record 会把 Scopes 转换成类方法。

# File activerecord/lib/active_record/scoping/named.rb, line 141
def scope(name, body, &block)
  unless body.respond_to?(:call)
    raise ArgumentError, 'The scope body needs to be callable.'
  end

  if dangerous_class_method?(name)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" "                "on the model \"#{self.name}\", but Active Record already defined "                "a class method with the same name."
  end

  extension = Module.new(&block) if block

  singleton_class.send(:define_method, name) do |*args|
    scope = all.scoping { body.call(*args) }
    scope = scope.extending(extension) if extension

    scope || all
  end
end

那么问题来了,既然可以使用类方法,为啥要使用 Scopes 来实现一样的功能呢?

当已经存在类方法的时候,何时使用 Scopes

例如:你希望根据特定过滤条件筛选出部分结果。当满足这些条件的结果集没有数据时,你希望返回全部数据。

如果使用 scope :

# app/models/review.rb

scope :created_since, ->(time) { where("reviews.created_at > ?", time) if time.present? }
scope :recent, -> { order("reviews.updated_at DESC") }
Review.created_since(nil).recent
# SELECT "reviews".* FROM "reviews" ORDER BY reviews.created_at DESC

Review.created_since('').recent
# SELECT "reviews".* FROM "reviews" ORDER BY reviews.created_at DESC

使用类方法:

def self.created_since(time)
  where("reviews.created_at > ?", time) if time.present?
end

def self.recent
  order("reviews.updated_at DESC")
end
Review.created_since('').recent
NoMethodError: undefined method `recent' for nil:NilClass

Scopes 帮我们做了一些额外的工作,因为它默认会返回 scope,所以我们可以使用方法链:

Review.positive.created_since(5.days.ago)

但是使用类方法,则需要手动处理意外情况(你需要自己处理 timenil 的情况)。

# app/models/review.rb

def self.created_since(time)
  if time.present?
    where("reviews.created_at > ?", time)
  else
    all
  end
end

方法应该返回同样的对象,这样你就永远不用担心边界情况和意外错误。你可以假设你永远都会拿到你期待处理的对象。

回到我们的例子中,这意味着在使用方法链的时候,你不用担心其中某一个环节出现 nil 的情况。

但是仍有一些临时工写的代码,让我们破坏 Scopes:

# app/models/review.rb

scope :broken, -> { "Hello!!!" }
irb(main):001:0> Review.broken.most_recent(5)
NoMethodError: undefined method `most_recent' for "Hello!!!":String

Scopes 是可以扩展的

以常见的分页 gem kaminari 为例:

Post.page(2).per(15)

posts = Post.page(2)
posts.total_pages # => 2
posts.first_page? # => false
posts.last_page?  # => true

我们可以给 Scopes 增加扩展功能,这些功能只有在该 Scope 被调用之后才能使用。

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

在 Scopes 内部提供扩展方法是一种灵活强大的技术,当然我们也可以使用类方法来实现一样的功能。

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

什么时候使用类方法,而不是 Scopes

Scopes 通常被用来实现一些简单的过滤,然后用方法链关联在一起取出正确的对象集合。

下面的两种情况,不建议使用 Scopes:

换句话说当你的 Scopes 变得复杂时候,就是时候把它变成类方法了。

在类方法里,可以混合使用 Ruby 代码和数据库方法。比如,你可以在先从数据库中取出结果集,然后通过 Ruby 的 sort_by 方法来排序。

你也可以在类方法里面,从不同的地方取出结果。比如 Redis,Database,第三方API。然后把结果集合并成为一个 Ruby 的集合。

当然,你可以在 Scopes 里面使用 selecting, sorting, joining, and filtering 的代码,然后在类方法中使用这些 scopes。最后你的类方法会变得很清晰,并且 scopes 可以在其他地方复用。

相关连接

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @Victor Sep 6, 2015

奉献爱心