'拦截'ActiveRecord ::关系的ARel而不会杀死一只小猫

时间:2012-11-30 22:37:00

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

在我正在创建的gem中,我想允许开发人员使用经典的Devise语法添加我编写的类方法,让我们称之为interceptor,模型:

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比使用正常情况更糟糕。

1 个答案:

答案 0 :(得分:1)

毕竟,我最终攻击了ActiveRecord::Relation的{​​{1}},但事实并非如此糟糕。这是从开始到结束的完整过程。

我的gem定义了一个method_missing类,旨在成为开发人员可以子类化的DSL。此对象从Interceptorroot获取一些Model ARel,并在呈现之前进一步操纵查询。

Relation

实现:

# 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

仅凭这一点,您可以调用User.interceptor并使用干净查询构建# 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 作为所有拦截器查询操作的根。但是,稍加努力,我们可以扩展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 > (?)', Time.current - 2.weeks).custom_interceptor DSL上设置所有范围设置,并在您在模型上构建的任何查询之上。