委派动态查找器

时间:2012-03-16 17:36:44

标签: ruby-on-rails ruby activerecord

我有一些模型代表用户可以获得的各种积分:

class User
  has_many :totals
end

class Total
  belongs_to :user
  belongs_to :total_type
end

class TotalType
  has_many :totals
end

还有pointsgoals等命名类型。我在Total:

中添加了一个不错的method_missing
def method_missing(method, *args, &block)
  if TotalType.find_by_name(method)
    joins(:total_type).where(:total_type => { :name => method }).sum(total)
  else
    super
  end
end

这让我做了一些甜蜜动态的东西,比如:

user = User.first
user.totals.points #=> 100
user.totals.goals  #=> 10

现在困难的部分。我希望能够直接在User上调用相同的pointsgoals方法。如果我使用delegate,那么我需要提前知道方法的名称:

class User
  # ...
  delegate :points, :totals, :to => :totals
end

但当然我更喜欢那也是动态的。我尝试在用户上创建method_missing,但当我尝试将send某些内容发送到totals时,我得到NoMethodError: undefined method 'points' for #<Array:0x007fd2016e1510>所以显然关联代理已将结果集转换为数组(遗漏了super以保持示例简单):

class User
  #...
  def method_missing(method, *args, &block)
    totals.send(method, *args, &block)
  end
end  

user = User.first
user.totals.points  #=> 100
user.points  #=> NoMethodError: undefined method 'points' for #<Array:0x007fd2016e1510>

如何将此调用转发到totals,而无需对这些动态查找器名称进行硬编码?任何帮助将不胜感激,谢谢!

2 个答案:

答案 0 :(得分:1)

是的,使用named_scope(或类方法)是最好的选择。

例如,如果这是totals.rb:

class Total < ActiveRecord::Base
  belongs_to :user
  belongs_to :total_type

  def self.points
    joins(:total_type).where(:total_types => {:name => 'points'}).sum(:total)
  end
end

这是users.rb:

class User < ActiveRecord::Base
  has_many :totals

  def method_missing(name, *args, &blk)
    if totals.respond_to? name
      totals.send(name, *args, &blk)
    else
      super
    end
  end
end

然后User.first.points#=&gt; 100

你可以让它变得更加动态......但代码更难以遵循,如果你不缓存一些东西,那么就会执行大量不必要的SQL查询。例如,total.rb可能是这样的:

class Total < ActiveRecord::Base
  belongs_to :user
  belongs_to :total_type

  def self.method_missing(name, *args, &blk)
    if TotalType.find_by_name(name.to_s)
      joins(:total_type).where(:total_types => {:name => name.to_s}).sum(:total)
    else
      super
    end
  end

  def self.respond_to_missing?(name, priv)
    if TotalType.find_by_name(name.to_s)
      true
    else
      super
    end
  end

end

因此,当调用User.first.points时,首先调用User#method_missing,看看user.totals.respond_to? :点。它起初并没有,所以调用Total :: respond_to_missing,看看是否有一个名为&#39; points&#39;的total_type。有...所以它被调用然后调用Total :: method_missing,它会返回该用户的点数之和。

当然,您可以轻松地缓存结果,因此每次在method_missing和respond_to_missing方法中调用user.points时都不会执行不必​​要的SQL查询,但它只是变得过于复杂而且方法开销不是很高。值得。我之前提供的不那么动态的解决方案是你最好的选择,现在你的模型如何布局。

希望这有帮助,

路加福音

答案 1 :(得分:0)

如果关注动态,像by_total_type这样的命名范围可能会解决问题。

class Total

  scope :by_total_type, lambda { |ttype| joins(:total_type).where(:type => ttype) }

end

然后你可以说:

Total.by_total_type('point')

或者

user.totals.by_total_type('goal')