先看个现象:

class User
  include Mongoid::Document
  has_many :orders
end
class Order
  include Mongoid::Document
  belongs_to :user
end

user.orders.pluck :id         # 正常返回id
user.orders.try :pluck, :id   # 返回 nil

奇怪的是第二行居然返回niluser.orders.try :pluck, :id返回nil大致有以下三种可能:

  1. user.orders返回nil,不过由于user.orders.pluck :id可以正常返回 id,排除这种可能;
  2. orders.pluck :id 返回 nil,同上,也可排除这种可能;
  3. user.orders.try :pluck, :id中的user.orders上没有pluck方法。这看起来是最不可能的情况,但是…

为了弄清原因,再来做个试验:

user.orders.try! :pluck, :id  # NoMethodError: undefined method `pluck' for #<Array:0x0...>

这次我们用的是try!而非try,其结果正好符合上面的第3中情况。唯有这样假设方能解释得通:user.orders.pluck :id中的user.orders返回的是Mongoid::Criteria;而user.orders.try :pluck, :id中的user.orders返回的是Array。下面我们来借助byebugRubyMine看看是否如此:

  1. 光标hover至一处调用pluck的地方,Go to Declaration(⌘B),搜索找到mongoid中的定义gems/mongoid-5.0.1/lib/mongoid/contextual/mongo.rb:399,加上byebug:
  def pluck(*fields)
    byebug
    normalized_select = fields.inject({}) do |hash, f|
      hash[klass.database_field_name(f)] = 1
      hash
    end

    view.projection(normalized_select).reduce([]) do |plucked, doc|
      values = normalized_select.keys.map do |n|
        n =~ /\./ ? doc[n.partition('.')[0]] : doc[n]
      end
      plucked << (values.size == 1 ? values.first : values)
    end
  end
  1. 打开 Rails Console,运行user.orders.pluck :id,然后运行backtrace: Mongoid relation many
  2. 定位到gems/mongoid-5.0.1/lib/mongoid/relations/referenced/many.rb:414 :
  def method_missing(name, *args, &block)
    byebug
    if target.respond_to?(name)
      target.send(name, *args, &block)
    else
      klass.send(:with_scope, criteria) do
        criteria.public_send(name, *args, &block)
      end
    end
  end

再次借助byebug不难发现这里的target已经是orders数组了,其class为Mongoid::Relations::Targets::Enumerable。 由这里的代码得知:对于target支持的方法,直接在target上调用;target不支持的方法则在criteria调用。拿我们前面的例子来说: user.orders.pluck :id中的targetorders(Array)不支持pluck方法,所以其实是在user.orders返回的criteria上调用的pluck;而user.orders.try :pluck, :id,中的 targetuser.orders(Array)支持try方法,所以直接在targetuser.orders上调用了,又因Array上没有pluck,故而返回nil。

所以这样?

user.orders.try :map, &:id