rake db:migrateをRidgepoleで置き換える
ちょっとRidgepole試してみようかなと思って使いはじめた。rails書いていると他の人がmigrationふつうにしようとしてしまったりするだろうから、rake db:migrateのデフォルトの動作を無効にして、ridgepoleを動作させたい。
lib/tasks/ridgepole.rakeとかで、
# overwrite db:migrate or db:dump task 'db:migrate' => :environment do ENV['RAILS_ENV'] ||= "development" sh "bin/ridgepole -E#{ENV['RAILS_ENV']} -c config/database.yml --apply" sh "bin/ridgepole -E#{ENV['RAILS_ENV']} -c config/database.yml --export -o Schemafile" end task 'db:dump' => :environment do ENV['RAILS_ENV'] ||= "development" sh "bin/ridgepole -E#{ENV['RAILS_ENV']} -c config/database.yml --export -o Schemafile" end
applyした直後でexportするべきかはどうかなーと思ったりしつつ、db:dumpいちいち打つのがダルいことが多いのかな。ということでやることにした。
Gemfileで、activerecordのedgeを指定する
先に結論をまとめると、こうすればよいらしい。
gem 'arel', git: 'https://github.com/rails/arel.git' git 'https://github.com/rails/rails.git' do gem 'activerecord' end
ライブラリで、特定のバージョン以上にbundlerが指定されていて、それを一時的にtravisなどで、HEADでテストを走らせたい、というときにどうするか、ということですが、railsの場合には、
rails/ -------rails.gemspec -------activerecord/ -------------------activerecord.gemspec -------actionpack/ -------------------actionpack.gemspec
のようなディレクトリ構成になっていて、directoryのtopにgemspecがある場合には、gitオプションで指定すればよいのですが、それ以下のディレクトリはどう指定したらいいんだろうか、と悩んでいたのでした。
test with active_record edge. · 3233386 · walf443/activerecord-mysql-unsigned · GitHub
config/routes.rbを分割する
とあるサービスのconfig/routes.rbが肥大化してきたので、分割したくなっていたので、調べていた。
ググってみると色々方法があるらしいが、バージョンごとにやり方が違ったりしてげんなりする。
よく出てくるconfig.paths["config/routes"]をいじる方法は、どうもRails4からは削除されていて動かないようだ。
ということで黒魔術を使った。めでたしめでたし。
# config/routes/some_module.rb module SomeModule def self.apply(context) context.instance_exec do # 実行させたい処理を書く # resources :products do # member do # get 'short' # post 'toggle' # end # end end end end
# config.rb Dir[Rails.root.join('config/routes/*.rb')].each do |file| load file end Your::Application.routes.draw do SomeModule.apply(self) end
解説
Your::Application.routes.drawの実態は、ActionDispatch::Routing::RouteSet#drawというやつのようで、その実装は、次のようになっている。
# actionpack/lib/action_dispatch/routing/route_set.rb def draw(&block) clear! unless @disable_clear_and_finalize eval_block(block) finalize! unless @disable_clear_and_finalize nil end
#eval_blockの実装は、
def eval_block(block) if block.arity == 1 raise "You are using the old router DSL which has been removed in Rails 3.1. " << "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/" end mapper = Mapper.new(self) if default_scope mapper.with_default_scope(default_scope, &block) else mapper.instance_exec(&block) end end private :eval_block
なので、drawしたときに作成する&blockの実態は、instance_execのコンテキストで実行されているようだ。
with_default_scopeするケースがあるが、こちらの実装は、
# actionpack/lib/action_dispatch/routing/mapper.rb def with_default_scope(scope, &block) scope(scope) do instance_exec(&block) end end
となっていて、こちらもinstance_execになっている。
なので、コンテキストを受けとって、コンテキストに対してinstance_execしてやれば同じコンテキストで実行できるので、instance_exec以降は通常のroutesと同じことが記述できる。
開発環境で再起動しなくても読みなおせるようにloadで読みこんでいるが、config/routes.rbのtimestampを更新しないと再読みこみしないのがどうしたもんかなーという感じではある。
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++
rrails v1.0.0 released.
以前公開した、rails/rakeコマンドを超早くするrrailsですが、
http://d.hatena.ne.jp/walf443/20120427/1335482420
あれから、色々な方に使っていただいているようで、パッチをいくつかもらったりしてupdateしたりしています。
最近quark-zjuさんという方からたくさんpatchをいただいて、今までできなかったことができるようになった & 今までと非互換なところがでてきた、ということで紹介します。
rails console/serverができるように
今まで子プロセスへPTYの引きつぎをしていなかったため、rails console/serverなどが実行しても、こちらの入力をrrailsへ教えることができなかったのですが、できるようになりました。
個人的にはrails consoleあんまり使わないので対応しなくてもいいかなーと思って放置していたところですが、使えるとまぁ便利かなとは思います。
rrails-serverコマンドは非推奨になり、rrails startで起動するように
guard-rrailsつかっているとあまり関係ないですが、rrails-serverコマンドはそのうちなくなると思われるので、rrails startを使うようにしてください。
TCPSocketではなく、UNIXDomainSocketをデフォルトで使うように
rrailsとの通信は今までTCPでの通信でしたが、デフォルトではUnixDomainSocketを使うようになりました。
これにより、複数のプロジェクトでのrrailsの立ちあげが楽になると思われます。
一方で、rrailsを実行する際に、対象のプロジェクトのディレクトリへcdしないと適切に実行できなくなっています。
まとめ
使い方が色々かわっている(なるべくは互換性残すようにはがんばったつもりだけど...)ので、今まで使っていた方はあらためてREADMEを読みなおすことを推奨します。
http://walf443.github.com/rrails/
rackのアクセスログをfluentdへ投げるミドルウェア
railsのアクセスログは、とても機械的に解析しづらく、運用するにはあまりよろしくない。そこで、Rack::CommonLoggerを使ってアクセスログを別途とったりしようとしていたのですが、(一般的にはNew Rericとかをつかうのが普通なんでしょうか?)
ログのローテーションとか考え出すと、あまり良い方法が思いつかない、という関係で、
- syslogへ投げる
- fluentdへ投げる
というアイディアを思いついたのですが、syslogだと機械解析しづらいフォーマットになってしまったりあまり詳しい人が意外といなかったりする関係で、流行っているfluentdへ投げちゃえと思いRack::CommonLogger::Fluentというrackのミドルウェアを書いてみた次第。
https://github.com/walf443/rack-common_logger-fluent
まだ作り始めで、仕様をどうしようと悩んでいるのだけど、わりと需要はありそうな気がしていて、できるだけ使われるものにしたいのと、あとから後方互換性を崩すということはなるべくしたくないので、ご意見を募集中
ログのフォーマット
Rack:CommonLogger::Fluentでは、RackのリクエストからHashを構築し、それをFluent::Loggerを使い、fluentdへ投げる、という感じになっています。
- "content_length": レスポンスのCONTENT_LENGTHヘッダの値。Integerにするか、Stringにするか悩んでいる。Intergerがよいのかな
- "http_status": レスポンスのHTTP STATUS CODE。これもIntegerにするかStringにするか悩んでいる。Integerがよいのかな
- "accessed_at": 日付はどのフォーマットの文字列にするべきか?
- ヘッダが空だった場合の扱い。nil or ""で悩んでいる。どちらかというとnilかなー?
ActiveResourceを少し触ってみた
TwitterのAPIでRailsのActive Resouceを試してみる
http://blog.tkmr.org/tatsuya/show/318-twitter-api-rails-active-resouce
の影響を受けてActiveResourceを使ってみた。
上の記事と合わせて思ったことをつらつらと書きます。
多くの場合書くクラスの設定は共通しているのでサイトごとにベースのクラスを用意してやって小クラスはそれを継承してやってから使う方が色々と楽でよいと思う。つまり上の記事の例なら、
require 'highline' require 'logger' class Twitter::API < ActiveResource::Base USER = HighLine.new.ask('user:') PASSWD = HighLine.new.ask('passwd:') {|q| q.echo = '*' } self.site = "http://#{USER}:#{PASSWD}@twitter.com" self.logger = Logger.new($stderr) end class Twitter::API::Status < Twitter::API end Twitter::API::Status.find(:all, :from => :user_timeline) #=> ... # Log GET http://twitter.com:80/statuses/user_timeline.xml--> 200 OK (12220b 0.80s)
みたいな感じに仕える。また、クラスアクセサにloggerを渡してやるとアクセスするたびにログにはかれるのでrequestメソッドをHackする必要はない。
ちなみにActiveResource::StructのRDocを見る限りでは将来的にはログイン情報などはcredentialsとして渡せるようになりそう。つまり上記のは次のように書ける。
class Twitter::API < ActiveResource::Base self.site = 'http://twitter.com' self.credentials({ :username => HighLine.new.ask('user:'), :password => HighLine.new.ask('passwd:') {|q| q.echo = '*' } }) end
早速HighLineの出番がきた。HighLine++