ActiveRecordのSQLの実行箇所をSQLのコメントに入れる

arproxyを使うと、SQLにフックして色々書きかえることができて非常に便利ですね。

module Arproxy
  class QueryLocationCommentAppender < Arproxy::Base

    WHITE_LIST_WORD_RE = %r{^[a-zA-Z0-9\-_/:\?]+$}

    def execute(sql, name=nil)
      if ENV["ARPROXY_QUERY_LOCATION"]
        # SQL Injection対策
        # コメントの閉じ文字がパラメータに含まれていなければ大丈夫なはず
        if ENV["ARPROXY_QUERY_LOCATION"].index("*/").nil?
          # 制御文字や、マルチバイトの文字を防ぐため、文字種を制限する
          if ENV["ARPROXY_QUERY_LOCATION"] =~ WHITE_LIST_WORD_RE
            substr = ENV["ARPROXY_QUERY_LOCATION"]
            substr = substr.size >= 100 ? substr.slice(0, 100) : substr
            sql += " /* called from \"#{substr}\" */ "
          end
        end
      end
      super(sql, name)
    end
  end
end

Arproxy.configure do |config|
  config.adapter = "mysql2"
  config.use Arproxy::QueryLocationCommentAppender
end

Arproxy.enable!

みたいなのをconfig/initializers/arproxy.rbに書いておいて、ApplicationControllerのbefore_filterあたりにでも、

  def set_query_comment_url
    ENV["ARPROXY_QUERY_LOCATION"] = request.path_info
  end

こんなのをしこんでおけば、実行したページのURL情報がわかるので、mysqlのslow queryのログなどを見て、どのURLのクエリが重たいのか判別できる。
(なおSQL Injection対策はmysql限定(それ以外は調べてないので、注意))

情報の受けわたしに環境変数をつかってしまったが、グローバル変数でよい気もする。

また、開発時には、URLではなくて、どのmodelや、controller、viewから発行されているか知りたくなるので、

    def execute(sql, name=nil)
      stack_trace = caller.select {|i| i.inspect =~ %r{app/(models|controllers|view)} }.join("\t")
      if stack_trace
      # SQL Injection対策
      # コメントの閉じ文字がパラメータに含まれていなければ大丈夫なはず
        if stack_trace.index("*/").nil?
          substr = stack_trace
          sql += " /* called from \"#{substr}\" */ "
        end
      end
      super(sql, name)
    end

こんな感じにしてdevelopment.logを見つつ、発行元を確認するとよい。(だいぶ見づらいけど。

arproxy++