ActiveRecordで発行するSQLのログをDBに保存する方法
管理ページのSQL(更新系のUPDATEやINSERT等)を保存しておきたい要望があるだろう。
(管理者の操作ログを詳細にとる代わりの代替案として等)
そんなときのやり方です。
ActiveRecordのソースを見た限りそういった機能はなさそうだったので
ActiveRecordの定義を書き換えで対応してます。
ちなみにクラス変数使ってるので非スレッドッセーフです。
そのうちrailsがマルチスレッド対応した場合はスレッドローカルストレージを使ってスレッドセーフにする必要があります。(rubyでできるか知らないですけど)
DB定義
テーブル:sql_logs
id int ID sql longtext SQL文 url text 発行元URL client_id int クライアントのID(ユーザーIDや管理者ID等を保存する) run_time float 実行時間 created_at datetime 作成日 updated_at timestamp 更新日(いらないですが一応。。)
rubyソース
environment.rbに追加
# sqlのログをDBに保存 module ActiveRecord module ConnectionAdapters class AbstractAdapter def log_info_with_db_log(sql, name, runtime) if SqlLogManager.instance.enabled # last_insert_idがSqlLogのidになってしまうので退避しておく if @db_log_write_before_insert_id_enabled @db_log_write_before_insert_id = @connection.insert_id @db_log_write_before_insert_id_enabled = false end SqlLogManager.instance.save_log(sql, runtime) end return log_info_without_db_log(sql, name, runtime) end alias_method_chain :log_info, :db_log end class MysqlAdapter < AbstractAdapter def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) if SqlLogManager.instance.enabled @db_log_write_before_insert_id_enabled = true end super sql, name insert_id = @db_log_write_before_insert_id # SqlLogをとらない場合はinsert_idがとれてないので取得する unless SqlLogManager.instance.enabled insert_id = @connection.insert_id end # 退避しておいたlast_insert_idを返す。 id_value || insert_id end end class MysqlReplicationAdapter < MysqlAdapter def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: if SqlLogManager.instance.enabled @db_log_write_before_insert_id_enabled = true end ensure_master execute(sql, name = nil) insert_id = @db_log_write_before_insert_id # SqlLogをとらない場合はinsert_idがとれてないので取得する unless SqlLogManager.instance.enabled insert_id = @connection.insert_id end # 退避しておいたlast_insert_idを返す。 id_value || insert_id end end end end
モデル
class SqlLog < ActiveRecord::Base end class SqlLogManager include Singleton attr_accessor :enabled, :client_id, :url def save_log(sql, runtime) # showとselectの場合はログに書かない。 # sqlの実行でエラーが出た場合にもログが書かれるため、Mysql::Errorも外す。ここは各DBにあわせて書き換える必要あり if sql.starts_with?("SHOW") || sql.starts_with?("SELECT") || sql.starts_with?("Mysql::Error") return end # sqllogをsaveしたときのinser文をまたsqllogとして保存しようとして無限ループするため、一時的に保存を解除する enabled_tmp = self.enabled self.enabled = false log = SqlLog.new(:sql=>sql, :run_time=>runtime, :client_id=>client_id, :url=>url) log.save! self.enabled = enabled_tmp end end
コントローラ
class ApplicationController < ActionController::Base before_filter :bf_application def bf_application logm = SqlLogManager.instance logm.enabled = false #adminページのみSQLを保存する場合のif文。 #ApplicationControllerでなくadminコントローラに書く場合等は不要 if controller_path.starts_with?("admin") logm.enabled = true logm.url = request.request_uri logm.client_id = 1#ユーザーIDや管理者ID等をいれれる end end end
追記:2008/11/7
last_insert_idがSqlLogのidになってしまう不具合があったので修正しました。
きちゃない修正ですが。。
そもそもですが、この方法をとるのではなく
Railsレシピという本のレシピ59の
実行者と実行内容の記録(ActiveRecordのオブザーバを利用する方法)を採用するほうがスマートだと思いましたorz
ActiveRecordのコールバックでのトランザクション
名前空間付きコントローラーでの問題
たいていのサンプルだとコントローラーは1フォルダに全部作られているが、管理画面や、ユーザー画面ごとにフォルダ分けしたいとこだろう。
一応作れるみたいで、ちょいとつまづいたのでメモしときます。
(rails2.1.1で確認)
URL:XXX/u/indexにおいて
- XXX_controller.rb(親コントローラ)
- XXX/u_controller.rb(子コントローラ)
というファイル構成にしたい場合に
XXXの名前によって動かない場合があった。
例えばXXXがAの場合
a_controller.rb class AController < ApplicationController end a/u_controller.rb class A::UController < AController def index render :text=>"aaaa" end end
XXXのフォルダ名を適当にいくつか試した結果
NGな名前
a
aa
aaa
aaaaa
ab
abcd
abcdef
b
OKな名前
admin
abcde
babc
babcde
とりあえず規則性がわからないためこのような構成の名前空間付きコントローラーは使えなそうだ。(そのうち直るかもしれないけども)
どうしても使いたい場合はroutes.rbでコントローラごとに1個ずつ書いていけばいいが大変。。
map.connect '/a/u/:action/:id', :controller=>"/a/u"
恐らくこの問題は親のコントローラー名が子のコントローラー名と重なるのが問題になると思われる。
例えばa/uというURLの場合
a/controller.rbのuメソッドなのか
a/u_controller.rbのindexメソッドなのかという問題もあるだろう。
なので下記のようなのにすることで解決した。
a/root_controller.rb(親)
a/u_controller.rb(子供)
親クラスの名前を子供の名前と重ならないようにする感じだ。
とりあえずrootって名前をつけたけどなんでもよい。
a/root_controller.rb class A::RootController < ApplicationController end a/u_controller.rb class A::UController < A::RootController def index render :text=>"aaaa" end end
これでとりあえず進めよう。
ちなみにこれの言い方ってどれがメジャーなんだろう。。
適当に思いつくもの一覧