在Rails中实现更简单的find_by_ [attribute]代码

时间:2012-11-30 20:06:56

标签: ruby-on-rails ruby ruby-on-rails-3 metaprogramming

我有一个状态表,每个状态都有一个name属性。目前我可以这样做:

FooStatus.find_by_name("bar")

那没关系。但我想知道我能否做到:

FooStatus.bar

所以我采用这种方法:

class FooStatus < ActiveRecord::Base
  def self.method_missing(meth, *args, &block)
    if self.allowed_statuses.include?(meth.to_s.titleize)
      self.where("name = ?", meth.to_s.titleize).first
    else
      super(meth, *args, &block)
    end
  end

  def self.allowed_statuses
    self.pluck(:name)
  end
end

上面的代码有效,但它会导致以下奇怪的行为:

FooStatus.respond_to?(:bar) => false
FooStatus.bar => #<FooStatus name: 'bar'>

那不是很好,但是如果我尝试实现respond_to ?,我会收到递归问题

class FooStatus < ActiveRecord::Base
  def self.method_missing(meth, *args, &block)
    if self.allowed_statuses.include?(meth.to_s.titleize)
      self.where("name = ?", meth.to_s.titleize).first
    else
      super(meth, *args, &block)
    end
  end

  def self.allowed_statuses
    self.pluck(:name)
  end

  def self.respond_to?(meth, include_private = false)
    if self.allowed_statuses.include?(meth.to_s.titleize)
      true
    else
      super(meth)
    end
  end
end

这让我:

FooStatus.bar => ThreadError: deadlock; recursive locking

有关让method_missing和respond_to一起工作的想法吗?

3 个答案:

答案 0 :(得分:1)

我不知道我是否推荐你的方法......对我来说似乎太神奇了,我担心当你的状态名称为'destroy'或者你可能合法地想要的其他方法时会发生什么调用(或内部的Rails调用,你不知道)。

但是......我觉得你最好通过循环使用allowed_statuses并创建它们来扩展类并自动定义方法,而不是忽略方法。这将使respond_to?工作。你还可以检查以确保它还没有在其他地方定义......

答案 1 :(得分:1)

我同意Philip Hallstrom的建议。如果您在构建类时知道allowed_statuses,那么只需遍历列表并明确定义方法:

%w(foo bar baz).each do |status|
  define_singleton_method(status) do
    where("name = ?", status.titleize).first
  end
end

...或者如果您需要代码中其他位置的状态列表:

ALLOWED_STATUSES = %w(foo bar baz).freeze
ALLOWED_STATUSES.each do |status|
  define_singleton_method(status) do
    where("name = ?", status.titleize).first
  end
end

更清晰,更短,更不容易出现未来破损和奇怪的兔子洞与ActiveRecord的冲突,就像你所在的那样。

你可以使用method_missing和朋友做很酷的事情,但这不是第一种进行元编程的方法。在可能的情况下,明确通常会更好。

我也同意菲利普关于用内置方法创造冲突的关注。拥有一个硬编码的状态列表可以防止这种情况走得太远,但如果该列表可能会增长或发生变化,您可能会考虑FooStatus.named_bar而不是FooStatus.bar这样的约定。

答案 2 :(得分:0)

使用范围。

class FooStatus < ActiveRecord::Base
  scope :bar, where(:name => "bar")

  # etc
end

现在,您可以执行FooStatus.bar,它将返回ActiveRelation对象。如果您希望此返回单个实例,则可以执行FooStatus.bar.firstFooStatus.bar.all,或者您可以将.first.all放在范围的末尾在哪种情况下,它会返回与取景器相同的东西。

如果输入不是常数(不总是&#34; bar&#34;),您还可以使用lambda定义范围。 Section 13.1 of this guide has an example