"拦截"活动记录::关系的ARel而不杀死小猫



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

class User < ActiveRecord::Base
has_interceptor
end

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

然而,我想找到一种优雅的方式,首先允许开发人员确定拦截器执行的查询的范围。这可以通过允许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的想法比正常情况更糟糕。

我最终破解了ActiveRecord::Relationmethod_missing,结果并不是太难看。以下是从开始到结束的整个过程。

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

# 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 > (?)', Time.current - 2.weeks).custom_interceptor将把InterceptorDSL中设置的所有作用域应用到您在模型上构建的任何查询之上。

相关内容

  • 没有找到相关文章

最新更新