先来一个简单的unscope

[8] pry(main)> named_c = Student.where(name: 'c')
  Student Load (0.1ms)  SELECT "students".* FROM "students" WHERE "students"."name" = ?  [["name", "c"]]
[9] pry(main)> binding.trace_tree(htmp: 'rails/unscope'){ named_c.unscope(where: :name)}
  Student Load (0.3ms)  SELECT "students".* FROM "students"


调用栈如下

20170729_150930_466_unscope.html

源码如下

def unscope(*args)
  check_if_method_has_arguments!(:unscope, args)
  spawn.unscope!(*args)
end

def unscope!(*args) # :nodoc:
  args.flatten!
  self.unscope_values += args

  args.each do |scope|
    case scope
    when Symbol
      if !VALID_UNSCOPING_VALUES.include?(scope)
        raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
      end
      set_value(scope, nil)
    when Hash
      scope.each do |key, target_value|
        if key != :where
          raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
        end

        target_values = Array(target_value).map(&:to_s)
        self.where_clause = where_clause.except(*target_values)
      end
    else
      raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example."
    end
  end

  self
end


被记忆的unscope_values

首先,保存unscope_values,主要是为了配合merge使用,因为根据注释所说的

# This method is similar to #except, but unlike
# #except, it persists across merges:
#
#   User.order('email').merge(User.except(:order))
#       == User.order('email')
#
#   User.order('email').merge(User.unscope(:order))
#       == User.all
#
# This means it can be used in association definitions:
#
#   has_many :comments, -> { unscope(where: :trashed) }


这可以通过跟踪merge来看看

[15] pry(main)> binding.trace_tree(htmp: 'rails/merge_unscope'){ named_c.merge(Student.unscope(where: :name))}
  Student Load (0.1ms)  SELECT "students".* FROM "students"


调用栈如下

20170729_160755_372_merge_unscope.html

源码如下

NORMAL_VALUES = Relation::VALUE_METHODS -
                Relation::CLAUSE_METHODS -
                [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering]

def normal_values
  NORMAL_VALUES
end

def merge
  normal_values.each do |name|
    value = values[name]
    # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
    # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
    # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
    # don't fall through the cracks.
    unless value.nil? || (value.blank? && false != value)
      if name == :select
        relation._select!(*value)
      else
        relation.send("#{name}!", *value)
      end
    end
  end

  merge_multi_values
  merge_single_values
  merge_clauses
  merge_preloads
  merge_joins

  relation
end


而normal_values则是

[16] pry(main)> ActiveRecord::Relation::Merger::NORMAL_VALUES
=> [:eager_load, :select, :group, :left_joins, :left_outer_joins, :references, :extending, :unscope, :limit, :offset, :readonly, :distinct]


所以merge会在新spawn的relation上再做一次unscope,而except则没这回事

[17] pry(main)> named_c.except(:where)
  Student Load (0.2ms)  SELECT "students".* FROM "students"


源码如下,可推导出,因为except并不记录你到底except过啥,而values的hash在删掉了:where后(不像unscope那样set_value scope, nil),merge就无法把except声明复写到新的relation里去了

def except(*skips)
  relation_with values.except(*skips)
end

private

  def relation_with(values)
    result = Relation.create(klass, table, predicate_builder, values)
    result.extend(*extending_values) if extending_values.any?
    result
  end


可接受的参数

记录完unscope_values后,就开始真正的解除scope了。从源码来看,参数可有两种类型,Symbol和Hash。Symbol可接受如下值

[10] pry(main)> named_c.unscope(:abc)
ArgumentError: Called unscope() with invalid unscoping argument ':abc'. Valid arguments are :where, :select, :group, :order, :lock, :limit, :offset, :joins, :includes, :from, :readonly, :having.
from /home/z/.rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/activerecord-5.1.2/lib/active_record/relation/query_methods.rb:399:in `block in unscope!'


而Hash则可用来解除具体的where条件的,Hash的值可以是单个字段,也可以是字段的集合

[19] pry(main)> named_c.where(gender: 'male').unscope(where: [:name, :gender])
  Student Load (0.2ms)  SELECT "students".* FROM "students"


小结

简单来说,一个relation内部就如同一个Hash:{where: {name: 'jack', gender: 0}, order: {created_at: :desc}},而unscope(:order, where: :name)所做的就是复制这个Hash,然后将改造为{where: {gender: 0}, order: nil},这样merge的时候,order会再被设置为nil,而新relation的where则在merge_clauses中整个替换掉。

而except因为已经删掉了key,所以merge时无法复写relation

感觉注释举例unscope(where: :trashed) 不好,应为unscope(:order),因为merge_clauses是整个替换where的,而order才会在走normal_values.each时复写为nil