2012-11-30 25 views
2

在我所創造的寶石中,我想讓開發人員將我編寫的類方法添加到一個模型中,我們稱之爲interceptor,語法:'攔截'ActiveRecord :: Relation's ARel不殺死小貓

class User < ActiveRecord::Base 
    has_interceptor 
end 

這使您可以撥打User.interceptor,它返回一個Interceptor對象,確實神奇事情經過Squeel寶石查詢數據庫。都好。

但是,我想找到一種允許開發人員首先對攔截器執行的查詢範圍進行優化的方式。這可以通過允許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鏈和竊取它的阿雷爾,它傳遞到我的Interceptor對象之前,我評價它來修改它。但是我所能想到的所有方式都涉及到代碼如此可怕,我知道如果我實施它,上帝會殺死一隻小貓。我手上並不需要那血跡。幫我保存一隻小貓?

問題:

添加到我的併發症,

class User < ActiveRecord::Base 
    has_interceptor :other_interceptor_name 
end 

允許你打電話User.other_interceptor_name和模型可以有多個攔截器。它運作良好,但使用method_missing比平常更糟糕的想法。

+1

看起來有'Interceptor'子類'關係'可能是要走的路,但我不想承諾,直到我找出更多的'關係'。 –

+0

我沒有答案,但如果你讓我連鎖:攔截器我會假設我可以把它放在鏈中的任何地方,它仍然會工作。真的嗎?否則,讓我將它用作範圍鏈的一部分可能有點誤導,但結果本身並不是一個範圍... –

+0

它的作用類似於'.all'或'.first',因爲它是因爲它評估查詢並返回一個非'關係'。但它的作用與'.where'相似,因爲它在評估之前修改了查詢,因此必須在某個時刻訪問該鏈。 –

回答

1

我結束了黑客行爲ActiveRecord::Relationmethod_missing畢竟它沒有變得太難看。這是從頭到尾的完整過程。

我的寶石定義了一個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 > (?)', Time.current - 2.weeks).custom_interceptor將適用於所有的作用域在Interceptor DSL設立您在模型上建立的任何查詢的頂部。