ActiveRecord の例外処理

RailsのActiveRecordで、save! とかすると保存に失敗したとき例外を投げてくれる。例えば、User.create!(params[:user]) とかして、失敗すると、ActiveRecord::RecordInvalid を投げてくれるのだけど、エラーをどうやって取るのか調べたのでメモ。

よくある例で、ユーザが作れたら index へ遷移して、作れなかったら new で再入力させるという場合。

createを使う場合

def create
  if @user = User.create(params[:user])
    flash[:notice] = 'ユーザ作成しました'
    redirect_to :action => 'index'
  else
    # ここでは、@user.errors でエラーが取れる
    render :action => 'new'
  end
end

create!を使う場合

def create
  @user = User.create!(params[:user])
  flash[:notice] = 'ユーザ作成しました'
  redirect_to :action => 'index'
rescue ActiveRecord::RecordInvalid => e
  # @user は nil になっているので、 @user.errors でエラーが取れない。
  render :action => 'new'
end

としたとき、create! で例外が飛んでいるので、当然 @user は nil となってます。なので、rescue 句の中で @user.errors ではエラーが取れません。

調べたところ、e.record で、ActiveRecord の例外を発生させたインスタンスが取れるようです。

def create
  @user = User.create!(params[:user])
  flash[:notice] = 'ユーザ作成しました'
  redirect_to :action => 'index'
rescue ActiveRecord::RecordInvalid => e
  # e.record に Userクラスのインスタンスが入ってる。
  @user = e.record
  render :action => 'new'
end

たとえば、View内で

< %= error_messages_for :user %>

とかしたい場合は、@user = e.record として置けばエラーが楽に表示出来て良い。

ActiveRecord の例外処理

rake notes

プログラム中にコメントで

# TODO 退会のフローについてメール確認待ち 090420

とか書いたりするのだけど、Rails で使ってると rake で一覧を抽出出来る。

% rake notes
app/models/user.rb:
  * [267] [TODO] 退会のフローについてメール確認待ち 090420

よく見てみたら、TODO以外も拾ってくれるようだ。

% rake --tasks
rake notes                           # Enumerate all annotations
rake notes:fixme                     # Enumerate all FIXME annotations
rake notes:optimize                  # Enumerate all OPTIMIZE annotations
rake notes:todo                      # Enumerate all TODO annotations

FIXME, OPTIMIZE, TODO とある。

どんな時に使うのか調べてみたら、

FIXME
修正の必要あり
OPTIMIZE
最適化の必要あり
TODO
TODOのメモ

みたいな感じに使えばいいらしい。

なるほど。

rake notes

Rails2.2.2 で i18n 使ってみる

Rails2.2.2 で簡単に多言語化(ていっても日本語しかお世話にならなそうだけど)出来てすごい!

% sudo gem so -a http://gems.github.com
% sudo gem i amatsuda-i18n_generators
% ./script/generate i18n ja
% vi config/locales/active_record_ja.yml

すると、なかにごにょごにょ

ja:
  activerecord:
    models:
      user: 'ユーザ'
    attributes:
      user:
        name: '名前'
        email: 'メールアドレス'
....

みたいな感じで書くと、ARのエラーとか日本語されてる。お手軽ぽ。

Rails2.2.2 で i18n 使ってみる

Rails&lighttpdで大容量ファイルをダウンロード

Rails&Lighttpdの環境で動画とか大容量ファイルを高速にダウンロードさせるとき、Rails の send_file を使わずに Lighttpd の X-sendfile という仕組みを使うと良いようです。

lighttpd-1.4.15 で試しました。

通常のsend_file

send_file '/tmp/movie.wmv'

Lighttpdを使ったx-sendfile

# X_SENDFILE する
path = '/tmp/movie.wmv'
response.headers['Content-Type'] = "application/force-download"
response.headers['Content-Disposition'] = "attachment; filename=\"123.mmv\""  # 好きな名前でダウンロードさせる
response.headers["X-LIGHTTPD-SEND-FILE"] = path
response.headers['Content-length'] = File.size(path)
render :nothing => true

これだけではダメで、lighttpdの設定も変更する必要がある。

lighttpdにパスが通ってる環境では lighttpd が優先的に起動されるが、初回起動時に config/lighttpd.conf が生成されるはずで、以下のようなくだりがあるはず。

fastcgi.server      = ( ".fcgi" => ( "localhost" => (
  "min-procs"       => 1,
  "max-procs"       => 1,
  "socket"          => CWD + "/tmp/sockets/fcgi.socket",
  "bin-path"        => CWD + "/public/dispatch.fcgi",
  "bin-environment" => ( "RAILS_ENV" => "development" )
) ) )

ここに、 "allow-x-send-file" => "enable" を追加すると先ほどの X-LIGHTTPD-SEND-FILE が使えるようになります。追加しないと、エラーは起きないけど、1byteの空ファイルしかダウンロードされません。

設定すると、以下のようになります。

fastcgi.server      = ( ".fcgi" => ( "localhost" => (
  "allow-x-send-file" => "enable",
  "min-procs"       => 1,
  "max-procs"       => 1,
  "socket"          => CWD + "/tmp/sockets/fcgi.socket",
  "bin-path"        => CWD + "/public/dispatch.fcgi",
  "bin-environment" => ( "RAILS_ENV" => "development" )
) ) )

x-send-file の注意点ではないですが、IEでは ヘッダに Cache-Control: no-cache がついていた場合、SSLでファイルのダウンロードが出来ないという仕様があります。こういうときは、SSLを経由しないようにするか、no-cache をやめる必要があります。どういう仕様なんだろう…。

どういう条件かわからないが、ファイルが変更されても304 Not Modified が返されてしまうことがあることが発覚。強制的に、200を返してあげれば最新のファイルがダウンロードされます。逆に変更されていない場合は304を返したいので、そういう割り当てをすれば良いのだが。

render :status => 200

とかで、強制的に200を返せるようだ。

Rails&lighttpdで大容量ファイルをダウンロード

RSpec on Rails を使う

準備

% sudo gem install diff-lcs redgreen ruby-snarl ZenTest launchy rspec webrat autotest_screen --no-ri --no-rdoc

インストール

%  script/plugin install git://github.com/dchelimsky/rspec.git
% script/plugin install git://github.com/dchelimsky/rspec-rails.git
% script/generate rspec

Railsのバージョンを上げたら、上記のコマンドを実行しないとエラーが出るメソッドがあるので注意。

Rails2.2.2でエラーで動かなかった

vendor/plugins/rspec_on_rails/lib/spec/rails/example/rails_example_group.rb:4:
undefined method `cache_template_extensions=' for
ActionView::Base:Class (NoMethodError)

というエラーが出てテストが実行できなかった。
該当箇所をコメントアウトしても今のところ問題は起きていないので、このまま続けてみようと思う。

#ActionView::Base.cache_template_extensions = false

Growlと連携

まず、Growlの設定で「ネットワーク」の「受信される通知を開く」にチェックを入れる。

あと、growlnotify をインストールしていなかったらインストールする。Growl-1.1.4.dmg をマウントしたら、

% cd /Volumes/Growl-1.1.4/Extras/growlnotify
% sudo ./install.sh

という感じでインストールできます。

あとは、autotest の設定ファイルを ~/.autotest に記述する。growlとautotestを連携させる – Hello, world! – s21g の通りに設定させてもらう。必要あるか分からないけど、一応 Growl の再起動をして試したところ無事動作しました。

動かなかったら、export RSPEC=true をする必要がある。

RSpec on Rails を使う

Leopard に Rails環境

MacBookもSSDに入れ替えて完全にSSD厨な感じだけど、環境構築してたら MySQL + Ruby でつまづいたのでメモしとく。

Ruby は、MacPorts で 1.8.7 をインストール。MySQL は、MySQL公式のダウンロードページ から、Mac OS X 10.5 (x86_64) をインストールした(これが良くなかったみたい)。

このあと、いつものように

% sudo gem install mysql -- --with-mysql-config /usr/local/mysql/bin/
mysql_config
Building native extensions.  This could take a while...
Successfully installed mysql-2.7
1 gem installed

ってやってインストールするんだけど、

% script/console
=> Booting WEBrick...
dyld: lazy symbol binding failed: Symbol not found: _mysql_init
  Referenced from: /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle
  Expected in: dynamic lookup

dyld: Symbol not found: _mysql_init
  Referenced from: /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle
  Expected in: dynamic lookup

って出てしまう。

ぐぐってみると、同じ症状が出てる人がいた。Leopard MacbookにMySQLをインストール〜Railsで使えるようにするまで
(http://blog.digital-squad.net/article/92963519.html) を参考にさせて頂いて、試してみる

% sudo env ARCHFLAGS="-arch x86_64" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
Building native extensions.  This could take a while...
Successfully installed mysql-2.7
1 gem installed

しかし・・・

% script/console
Loading development environment (Rails 2.0.2)
/Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle: dlopen(/Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle, 9): no suitable image found.  Did find: (LoadError)
        /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle: mach-o, but wrong architecture - /Library/Ruby/Gems/1.8/gems/mysql-2.7/lib/mysql.bundle       from /Library/Ruby/Site/1.8/rubygems/custom_require.rb:31:in `require'
        from /Library/Ruby/Gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:496:in `require'
        from /Library/Ruby/Gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:342:in `new_constants_in'
        from /Library/Ruby/Gems/1.8/gems/activesupport-2.0.2/lib/active_support/dependencies.rb:496:in `require'
        from /Users/chu/rails/viena/config/environment.rb:97
        from /Library/Ruby/Site/1.8/rubygems/custom_require.rb:31:in `gem_original_require'
        from /Library/Ruby/Site/1.8/rubygems/custom_require.rb:31:in `require'
        from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/irb/init.rb:253:in `load_modules'
        from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/irb/init.rb:251:in `each'
        from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/irb/init.rb:251:in `load_modules'
        from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/irb/init.rb:21:in `setup'
        from /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/irb.rb:54:in `start'
        from /usr/bin/irb:13

違うエラーになった。

下記を試せばいいっていう書き込みもあったけど、どうやら、x86_x64 のMySQL を使うとこれが出るっぽい。

% sudo env ARCHFLAGS="-arch i386" gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
Building native extensions.  This could take a while...
Successfully installed mysql-2.7
1 gem installed

というわけで、Mac OS X 10.5 (x86) を改めて入れ直して、

% sudo gem install mysql -- --with-mysql-config=/usr/local/mysql/bin/mysql_config
Building native extensions.  This could take a while...
Successfully installed mysql-2.7
1 gem installed

ってやったら普通に動いた。うーん。

友達曰く、port で入れれば普通に動くのに。らしい(‘A`)

Leopard に Rails環境

Railsで同じ処理が並行して走らないようにする

 複数プロセスを立ち上げていても、script/runner 等で叩くときも並行して同じ処理を走らせないようにするとき、ファイルのロックを使うのが簡単です。

ソース

#{RAILS_ROOT}/lib/batch_lock.rb という名前で下記を保存する。

# 同一処理を並行で走らせない様にするモジュール
class BatchLockException < Exception; end
module BatchLock
  # 処理を排他ロックして走らせる
  def self.run(batch_name)
    FileUtils.mkdir(RAILS_ROOT + '/tmp/batch_lock') unless File.exists?(RAILS_ROOT + '/tmp/batch_lock')
    # batch_name をスコープとして排他処理される。
    File.open("#{RAILS_ROOT}/tmp/batch_lock/#{batch_name}", 'w') do |f|
      if f.flock(File::LOCK_EX | File::LOCK_NB)
        yield
      else
        raise BatchLockException.new
      end
    end
  end
 
  # 処理が走っているか確認する
  def self.running?(batch_name)
    FileUtils.mkdir(RAILS_ROOT + '/tmp/batch_lock') unless File.exists?(RAILS_ROOT + '/tmp/batch_lock')
    # ロック用のファイル名は、バッチごとにユニークにする。
    File.open("#{RAILS_ROOT}/tmp/batch_lock/#{batch_name}", 'w') do |f|
      ! f.flock(File::LOCK_EX | File::LOCK_NB)
    end
  end
end

使い方

同時に走らせたくない処理をブロックで実行する。

begin
  BatchLock.run(:go_to_park) do
    # ユーザがみんな公園に行く
    User.find(all).each(&:go_to_park)
  end
rescue BatchLockException
  logger.error "既にバッチが走っていた…。"
end

今バッチが走ってるか確認する。

p BatchLock.running?(:go_to_park) ? 'バッチ進行中' : 'バッチは走ってない'
Railsで同じ処理が並行して走らないようにする

500万件とかRailsでバッチ処理は結構きつい

 Railsで、ユーザが500万件居たりすると User.find(:all) とかやっただけでかなりきつい。こういうのをやるときって、バッチ処理だと思うんだけど、結構工夫してループ内で何回も find したりしても、メモリ足りなくなったりするから、結局 script/runner で Rails の起動コストを引いても シェルスクリプト回したほうが安全っていう結論になる。

 50000件ずつループで処理して、Railsを再起動みたいな感じになる。

#!/bin/sh
limit=50000
count=50000
while [ $count -le 5000000 ];
do
  script/runner "User.find(:all, :offset => $count,  :limit => $limit, :conditions => ['migration_complete = false']).each(&:check_email)"
 
  echo "$count"
  count=`expr $count + 5000`
done
500万件とかRailsでバッチ処理は結構きつい