Rails Rails 5.1 add delegate_missing_to

zhangbin-github for 时空 · 2017年06月02日 · 最后由 kafei 回复于 2017年12月21日 · 6305 次阅读
本帖已被管理员设置为精华贴

http://blog.bigbinary.com/2017/05/30/rails-5-1-adds-delegate-missing-to.html

rails 5.1 添加了 delegate_missing_to 方法

实验如下:

rails g model user usename:string password:string email:string
rails g model book title:string user:references

rails  db:migrate

生成如下 model

class User < ApplicationRecord
    has_many :books
end

class Book < ApplicationRecord
    belongs_to :user
end

在 console 中实验

User.create(username:'test', password:'123456', email:'[email protected]')
Book.create(user:User.last, title:'this is a book')
irb(main):017:0> Book.last.user
  Book Load (0.3ms)  SELECT  `books`.* FROM `books` ORDER BY `books`.`id` DESC LIMIT 1
  User Load (0.2ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> #<User id: 1, username: "test", password: "123456", email: "[email protected]", created_at: "2017-06-02 01:55:54", updated_at: "2017-06-02 01:55:54">

irb(main):018:0> Book.last.username
  Book Load (0.4ms)  SELECT  `books`.* FROM `books` ORDER BY `books`.`id` DESC LIMIT 1
NoMethodError: undefined method `username' for #<Book >
    from (irb):18

可以看到直接访问 username 会报 method missing

在 book 中添加 delegate_missing_to

class Book < ApplicationRecord
  belongs_to :user
  delegate_missing_to :user
end

try again

irb(main):020:0> Book.last.username
  Book Load (0.2ms)  SELECT  `books`.* FROM `books` ORDER BY `books`.`id` DESC LIMIT 1
  User Load (0.3ms)  SELECT  `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> "test"

看实现 pr: https://github.com/rails/rails/pull/23930/files#r64312481

# The target can be anything callable withing the object. E.g. instance
# variables, methods, constants ant the likes.
def delegate_missing_to(target)
  target = target.to_s
  target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

  module_eval <<-RUBY, __FILE__, __LINE__ + 1
    def respond_to_missing?(name, include_private = false)
      #{target}.respond_to?(name, include_private)
    end
    def method_missing(method, *args, &block)
      #{target}.send(method, *args, &block)
    end
  RUBY
end

好处:1:可以少写一点代码,在比较多的 delegate 指向同一个 asocciation 的时候;2:可以向其他非动态语言炫技

jasl 将本帖设为了精华贴。 06月02日 11:23

这种功能就是坑。。之后各种性能问题

tony612 回复

比如?还是你再说如果不清楚实现细节会有无谓的查询?

我之前在做扩展字段(基于serialize字段),如果要做到透明就是自己实现了这个功能

jasl 回复

人人都喜欢省事,但程序开发并不是可以偷懒的,有时 explicit 反而是更好的选择。当你调用一个方法,都很可能意识不到它做了 DB 查询时,最终就拖慢了你的程序。

不过我看了那个 PR,它给的 example 倒没有这个问题。

class Partition
  def initialize(first_event)
    @events = [ first_event ]
  end

  private
    def respond_to_missing?(name, include_private = false)
      @events.respond_to?(name, include_private)
    end

    def method_missing(method, *args, &block)
      @events.send(method, *args, &block)
    end
end
=>
class Partition
  delegate_missing_to :@events

  def initialize(first_event)
    @events = [ first_event ]
  end
end

只是既然楼主会想到这种用法,就难免会有很多人会这样用

tony612 回复

这个确实,不过问题在于限制 delegate_missing_to 的使用,其实主贴说的有点不太对,看指出的源码,这里的实现只是一个增强版的 delegate 而已,而 delegate 也有滥用会无意识增加额外查询的问题

jasl 回复

👍 我也刚刚看到

是我的用法有问题😂 请不要模仿

我有点不明白做这个 delegate_missing_to 的目的了,难道不是为了简化代码?毕竟 pr 里给出的例子,还是摆脱不了一次数据库查询啊

同意 @tony612 的看法,这种 feature 简直是毒瘤!

numbcoder 回复

这个要看怎么用吧,如果在遍历集合的时候使用 delegate,肯定会产生性能问题(N+1)

  1. 显式地 delegate 方法给特定的对象
  2. 借助 delegate_missing_to 一揽子地把公开方法 delegate 给特定对象,但并非是透明代理 ( transparent proxy )
  3. 借助 SimpleDelegator 来实现透明代理
  4. 借助 Forwardable 实现类似 delegate 的功能。

Pattern 有点多,LZ 来讲讲

Book.includes(:user).references(:user).last.username

可解决 n+1 的问题

我理解 delegate_missing_to 就是增强版的 delegate,将原本

delegate :username, to: :user
delegate :email, to: :user

简化,通过 method_missing 来 send,这个和 rails 本身的 delegate 实现还是有区别的

你这例子想坑死多少了。。。。book.username 你真的不怕维护着拿刀砍你吗?

pynix 回复

其实这东西适用场景挺多的,可以把组合模式透明化啊,不过把方法代理到另一个 AR 实体确实有坑... 而且 AR 没办法自动 eager loading

不光是性能的问题,项目做到很大,协作人员多了之后,后面接手项目的人会很想砍人

实现用到的method_missing本身就会拖慢性能,它会先找该对象的方法,再找所有父类,没有该方法再实现。元编程里也是尽量不用的

还是不能因为自己不会用就说新方法不好

numbcoder 回复

是项目做到很臃肿吧

huibin 回复

真的已经到了要抠一个 method_missing 的性能问题的地步吗?直接写汇编最快执行效率最高

lithium4010 回复

N+1 query 啊

这个就是 draper 的 delegate_all 实现,而 draper 就是装饰器模式的典型使用场景

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