【问题标题】:'Intercept' ActiveRecord::Relation's ARel without killing a kitten'拦截' ActiveRecord::Relation 的 ARel 而不杀死一只小猫
【发布时间】:2012-11-30 22:37:00
【问题描述】:

在我正在制作的 gem 中,我想允许开发人员将我编写的类方法添加到模型中,我们称之为 interceptor,采用经典的 Devise 语法:

class User < ActiveRecord::Base
  has_interceptor
end

这允许您调用User.interceptor,它返回一个Interceptor 对象,该对象通过Squeel gem 查询数据库来完成神奇的事情。都很好。

但是,我想首先找到一种允许开发人员确定拦截器执行的查询范围的优雅方法。这可以通过允许interceptor 接收ActiveRecord::Relation 并链接Squeel 来实现,否则就回退到模型上。此实现的工作原理如下:

# Builds on blank ARel from User:
User.interceptor.perform_magic
#=> "SELECT `users`.* FROM `users` WHERE interceptor magic"

# Build on scoped ARel from Relation:
User.interceptor( User.where('name LIKE (?)', 'chris') ).perform_magic
#=> "SELECT `users`.* FROM `users`  WHERE `users`.`name` LIKE 'chris' AND  interceptor magic"

这是有效的,但丑陋的。我真正想要的是:

# Build on scoped ARel:
User.where('name LIKE (?)', 'chris').interceptor.perform_magic
#=> "SELECT `users`.* FROM `users`  WHERE `users`.`name` LIKE 'chris' AND  interceptor magic"

本质上,我想“接入”ActiveRecord::Relation 链并窃取它的 ARel,然后将其传递到我的 Interceptor 对象中以在我评估它之前对其进行修改。但是我能想到的每一种方法都涉及到如此可怕的代码,我知道如果我实现它,上帝会杀了一只小猫。我不需要我手上的血。帮我救一只小猫?

问题:

增加了我的复杂性,

class User < ActiveRecord::Base
  has_interceptor :other_interceptor_name
end

允许你调用User.other_interceptor_name,模型可以有多个拦截器。它运作良好,但使用method_missing 比平常更糟糕。

【问题讨论】:

  • 似乎拥有Interceptor 子类Relation 可能是要走的路,但在我想出更多Relations 之前,我不想提交。
  • 我没有答案,但是如果你让我链接 :interceptor 我会假设我可以将它放在链中的任何位置并且它仍然可以工作。真的吗?否则,让我将它用作作用域链的一部分可能会有点误导,但结果本身不是作用域......
  • 它的作用类似于.all.first,因为它必然是链中的最后一个,因为它评估查询并返回非Relation。但它的行为类似于.where,因为它会在评估之前修改查询,因此必须在某个时候访问该链。
  • +1 表示“不杀小猫”。很好的问题标题!
  • 我正在关注ActiveRecord::Scoping's with_scope,明天得去取回。

标签: ruby-on-rails ruby-on-rails-3 activerecord arel


【解决方案1】:

我最终破解了ActiveRecord::Relationmethod_missing,毕竟它并没有变得太丑陋。这是从头到尾的完整过程。

我的 gem 定义了一个 Interceptor 类,旨在成为开发人员可以继承的 DSL。该对象从ModelRelation 接收一些root ARel,并在呈现之前进一步操作查询。

# gem/app/interceptors/interceptor.rb
class Interceptor
  attr_accessor :name, :root, :model
  def initialize(name, root)
    self.name = name
    self.root = root
    self.model = root.respond_to?(:klass) ? root.klass : root
  end
  def render
    self.root.apply_dsl_methods.all.to_json
  end
  ...DSL methods...
end

已实现:

# sample/app/interceptors/user_interceptor.rb
class UserInterceptor < Interceptor
  ...DSL...
end

然后我为模型提供has_interceptor 方法,该方法定义新的拦截器并构建interceptors 映射:

# gem/lib/interceptors/model_additions.rb
module Interceptor::ModelAdditions

  def has_interceptor(name=:interceptor, klass=Interceptor)
    cattr_accessor :interceptors unless self.respond_to? :interceptors
    self.interceptors ||= {}
    if self.has_interceptor? name
      raise Interceptor::NameError,
        "#{self.name} already has a interceptor with the name '#{name}'. "\
        "Please supply a parameter to has_interceptor other than:"\
        "#{self.interceptors.join(', ')}"
    else
      self.interceptors[name] = klass
      cattr_accessor name
      # Creates new Interceptor that builds off the Model
      self.send("#{name}=", klass.new(name, self))
    end
  end

  def has_interceptor?(name=:interceptor)
    self.respond_to? :interceptors and self.interceptors.keys.include? name.to_s
  end

end

ActiveRecord::Base.extend Interceptor::ModelAdditions

已实现:

# sample/app/models/user.rb
class User < ActiveRecord::Base
  # User.interceptor, uses default Interceptor Class
  has_interceptor
  # User.custom_interceptor, uses custom CustomInterceptor Class
  has_interceptor :custom_interceptor, CustomInterceptor

  # User.interceptors #show interceptor mappings
  #=> {
  #     interceptor: #<Class:Interceptor>,
  #     custom_interceptor: #<Class:CustomInterceptor>
  #   }
  # User.custom_interceptor #gets an instance
  #=> #<CustomInterceptor:0x005h3h234h33>
end

仅此一项,您就可以调用 User.interceptor 并构建一个 Interceptor,其中包含一个干净的查询作为所有拦截器查询操作的根。但是,只要稍加努力,我们就可以扩展 ActiveRecord::Relation,以便您可以将拦截器方法作为范围链中的端点调用:

# gem/lib/interceptor/relation_additions.rb
module Interceptor::RelationAdditions

  delegate :has_interceptor?, to: :klass

  def respond_to?(method, include_private = false)
    self.has_interceptor? method
  end

protected

  def method_missing(method, *args, &block)
    if self.has_interceptor? method
      # Creates new Interceptor that builds off of a Relation
      self.klass.interceptors[method.to_s].new(method.to_s, self)
    else
      super
    end
  end

end

ActiveRecord::Relation.send :include, Interceptor::RelationAdditions

现在,User.where('created_at &gt; (?)', Time.current - 2.weeks).custom_interceptor 将在您在模型上构建的任何查询之上应用在 Interceptor DSL 中设置的所有范围。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-08-01
    • 2013-05-27
    • 1970-01-01
    • 1970-01-01
    • 2017-10-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多