cookpadさんのArproxyのソースコードリーディングをしました。
機能の概要
ActiveRecordのDBへのアクセス前後に処理を入れることができるようになるプロキシ的なライブラリです。具体的な用途としては以下のように、ロガー、セキュリティ系で利用されるイメージです。
- スロークエリを抽出してログを書き出す
- 特定のクエリを禁止する(例えばSELECTだけ許可してDMLはNGとする)
使い方
READMEのコピペですが、プロキシクラスを定義してconfigureで設定してenable!で有効化します。class QueryTracer < Arproxy::Base
def execute(sql, name=nil)
Rails.logger.debug sql
Rails.logger.debug caller(1).join("\n")
super(sql, name)
end
end
Arproxy.configure do |config|
config.adapter = "mysql2" # A DB Apdapter name which is used in your database.yml
config.use QueryTracer
end
Arproxy.enable!
Arproxy::Baseはproxy_chain、next_proxyのプロパティとexecuteメソッドを持ちます。
module Arproxy
class Base
attr_accessor :proxy_chain, :next_proxy
def execute(sql, name=nil)
next_proxy.execute sql, name
end
end
end
Baseを継承したプロキシクラスはexecuteメソッドを実装しますが、その際に、super、つまりnext_proxy.executeを呼び出します。これによってプロキシクラスのexecuteの処理 => next_proxyのexecuteの処理、というように連鎖していき、最後にはActiveRecordのアダプタのexecuteメソッドにたどりつくという仕組みです。
Arproxy.configureではConfigクラスをイニシャライズしてからブロックに記述した通りに設定します。
module Arproxy
class Config
attr_accessor :adapter, :logger
attr_reader :proxies
def initialize
@proxies = []
end
def use(proxy_class, *options)
::Arproxy.logger.debug("Arproxy: Mounting #{proxy_class.inspect} (#{options.inspect})")
@proxies << [proxy_class, options]
end
def plugin(name, *options)
plugin_class = Plugin.get(name)
use(plugin_class, *options)
end
def adapter_class
raise Arproxy::Error, "config.adapter must be set" unless @adapter
case @adapter
when String, Symbol
camelized_adapter_name = @adapter.to_s.split("_").map(&:capitalize).join
eval "::ActiveRecord::ConnectionAdapters::#{camelized_adapter_name}Adapter"
when Class
@adapter
else
raise Arproxy::Error, "unexpected config.adapter: #{@adapter}"
end
end
end
end
Config内にはproxiesとadapterプロパティを持っており、proxiesはプロキシのクラスとオプションのリストとなっており、adapter_classは任意のDBに対するActiveRecordのアダプタのクラスを返すメソッドとなっており、@adapterにはアダプタクラスに対応する名前をセットすることになります。
enable!メソッド内ではProxyChainをイニシャライズして、@configを引数にしてenable!メソッドを呼び出します
@proxy_chain = ProxyChain.new @config
@proxy_chain.enable!
ProxyChainはイニシャライズ時にChainTailインスタンスを生成します。その名の通り、このインスタンスがexecuteチェーンの最後のインスタンスとなり、executeメソッドではActiveRecordのDBアダプタのexecuteメソッドを呼び出します。proxy_chain.connectionにはActiveRecordのDBアダプタのインスタンスがセットされます。
module Arproxy
class ChainTail < Base
def initialize(proxy_chain)
self.proxy_chain = proxy_chain
end
def execute(sql, name=nil)
self.proxy_chain.connection.execute_without_arproxy sql, name
end
end
end
以下でconfigに入れたproxiesをセットしていきます。
@head = @config.proxies.reverse.inject(@tail) do |next_proxy, proxy_config|
cls, options = proxy_config
proxy = cls.new(*options)
proxy.proxy_chain = self
proxy.next_proxy = next_proxy
proxy
end
Proxy1、Proxy2という順番で設定した場合、reverseにより逆順で処理されます。injectの初期値(最初のnext_proxy)はChainTailで、proxy_configはproxiesの最後に設定したプロキシクラスになります。順にプロキシクラスをインスタンス化してnext_proxyによって後ろからつなげていき最後に一番最初に設定したプロキシクラスを@headとしてセットします。
enable!メソッドではアダプタクラスのclass_evalで以下の処理を実行します。
- execute_with_arproxyメソッドを定義:ProxyChainのheadのexecuteを走らせる(ここからチェーンしてChainTailのexecuteまで処理を行う)
- もともとのexecuteメソッドのエイリアスとしてexecute_without_arproxyを定義
- execute_with_arproxyのエイリアスとしてexecuteを定義