新手问题 求助:当 undef_method 遇上 method_missing,rails 如何处理属性方法?

Lucia_ca · 2018年09月04日 · 最后由 Lucia_ca 回复于 2018年09月04日 · 280 次阅读

使用undef_method, 遇到的一个问题,想不明白rails是如何处理的,不知哪位大神可以给到一个解答。

场景如下:

user model含有字段name, 同时include了一个module UserAddon,使用undef_method来禁止调用引入的name方法,按照undef_method的定义, 此时User的对象调用name时,应该会报错,但是进入终端后,发现没有,反而返回了正确的结果。

代码如下:

## app/models/user.rb

# == Schema Information
#
# Table name: users
#
#  id                          :integer          not null, primary key
#  email                       :string(254)
#  name                        :string(254)
#  password_digest             :string(254)
#  created_at                  :datetime         not null
#  updated_at                  :datetime         not null

class User < ActiveRecord
  include UserAddon
  undef_method :name
end

## app/models/user_addon.rb
module UserAddon
  def name
    "hello"
  end
end

这时,rails c 进入console:

user = User.first

# => #<User id: 1, name: "admin", email: "test@gmail.com", created_at:……>

user.methods.include?(:name)
# => false

user.name

# => "admin"

$ user.name

# => Couldn't locate a definition for user.name

user.respond_to?(:name)
# => true

使用pry的$ ,找不到user.name的定义,但是执行user.name却返回了正确的结果,此时一定调用了method_missing。但是,如何查看rails是如何实现的呢?

ruby元编程中在有关 rails的属性方法中曾经提到,当第一次访问一个属性时,这个属性是一个幽灵方法,ActiveRecord::Base # method_missing() 会把它转换成一个真实的方法,同时创建出读,写,查询方法,比如上面的name,会创建name, name=, name?,这样下次就可以直接调用。

但是,这里很显然,user.methods.include?(:name)返回了false,这里并没有创建name这个方法,那么rails是如何让user.name 返回了"admin"的呢?

我看了下rails的attribute_methods中method_missing部分的代码,苦于水平有限,没看懂这里到底是如何实现的,不知哪位大神可以给到一个清晰的解答或者指个方向? thx!

共收到 6 条回复

不 include UserAddon 看看 User.methods 里面有没有 name 和 name=

IChou 回复

不 include UserAddon时,使用undef_method :name 会报错,显示user没有name这个方法,如果include UserAddon和undef_method :name 都注释掉,那就跟普通的rails对象一样了,有name 和name=,我这里只undef_method :name 了,对name=没有影响的。上面的代码不变动,进入rails console:

user = User.first
user.name
# => "admin"
user.name = "admin2"
user.name
# => "admin2"

name= 是正常的,执行user.methods.include?(:name=) 也返回true

没去看源码 做了个小实验

class User < ActiveRecord
end

User.instance_methods.include?(:name) #=> false
user = User.new
User.instance_methods.include?(:name) #=> true

user[:name] = 'admin'

User.class_eval %q{undef_method :name}
User.instance_methods.include?(:name) #=> false
user.methods.include?(:name) #=> false
user.name #=> "admin"

看上去 attributes 的 methods 是懒加载的,只有在调用相关内容的时候才添加到 Class 里面,而且就算你把它从类里面 undef 掉,当你在实例上调用它时,它依旧会通过 method_missing 的 fallback 表现出正常的行为,应该算一种安全机制吧

IChou 回复

谢谢解答!好像比之前明白了一些。其实我好奇的就是rails是如何通过method_missing的fallback来让user.name 返回正常值的,不过实在水平有限,看源代码看的懵逼。不纠结了,先记着,说不定以后会懂的,谢谢您的回复😃

看了下源码,如果没有 undef_method :name 就如你说的,会在第一调用的时候生成各种方法追加到 User 的实例方法中去,这个时候你调用 user.name 是不会触发 method_missing 的

现在来说你 undef_method :name 的情况,首先它会落入下面的 method_missing 方法中

# lib/active_model/attribute_methods.rb:425

def method_missing(method, *args, &block)
  if respond_to_without_attributes?(method, true) # =>  这里是 false
    super
  else
    match = matched_attribute_method(method.to_s)
    #<struct ActiveModel::AttributeMethods::ClassMethods::AttributeMethodMatcher::AttributeMethodMatch target="attribute", attr_name="name", method_name="name">
    match ? attribute_missing(match, *args, &block) : super
  end
end

def attribute_missing(match, *args, &block)
  __send__(match.target, match.attr_name, *args, &block)
  # match.target => "attribute", 这里调用了自身的 attribute 方法
end

# lib/active_record/attribute_methods/read.rb:76
def _read_attribute(attr_name) # :nodoc:
  @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
end
alias :attribute :_read_attribute
private :attribute

这下明白了吧

IChou 回复

如此耐心的解答,谢谢!!最后是通过@attributes.fetch_value(“name”)得到的,是吗?原谅我比较小白,我很好奇,您是如何执行这段代码的?我当时看这块的时候,还在想着如何调用这个method_missing, 把name作为参数传过去,然后看它到底是怎么调用的,类似设置断点binding.pry, 然后next, next,可以看到一步步做了什么的那种,但是我在终端使用pry的$完全没有效果,因为.name不是一个method。常规下,如果一段代码调用了method_misssing,请问我可以通过什么方式查看到它具体的调用步骤?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册