Rails tips

明示してなければ 2.3.8。

Ruby については Ruby tips 参照。

モデル

file_column 保存時に MIME タイプを取得

file_column の仕組みについては file_column プラグイン内部構造 - Rails で行こう! がわかりやすい。

# ActiveRecord::Base のサブクラス内
file_column :file

def file_with_getting_content_type=(file)
  self.mime = file.content_type #=> 'image/png' とか
  self.file_without_getting_content_type = file
end
alias_method_chain :file=, :with_getting_content_type

default_scope は危険

Rails 3 から使える default_scope は一見便利だが、注意が必要。

class Entry < ActiveRecord::Base
  default_scope order("created_at DESC")
end

などとしていると、

 Entry.order("created_at ASC").all

としても、発行される SQL は

 SELECT * FROM entries ORDER BY created DESC, created ASC

となり、期待したようにはならない。 もちろん unscoped で default_scope を解除することもできる。

 Entry.unscoped.order("created_at ASC").all

が、unscoped はそれまでの scope をすべて解除してしまうので、

 class Blog < ActiveRecord::Base
   has_many :entries
 end
 
 blog = Blog.first
 blog.entries.unscoped.order("created_at ASC").all

などとすると、blog.id が一致しない Entry まで引っ張ってきてしまう。

コントローラ

リクエストメソッドを処理する場合

request.methodrequest.request_method を使う。

  • request.method はクライアントから送られてきたリクエストメソッドそのもの。
  • request.request_method_method パラメータを考慮したもの。

通常は request.request_method を使う。

ファイルを返す

render のかわりに send_file / send_data を使う。

  • send_file にはファイルパスを渡す。ファイルが実際にある場合はこちらが楽
  • send_data にはバイナリデータを (String オブジェクトで) 渡す。ファイルがなくてもよい。

いづれも :disposition オプションで、インラインで表示させるか、ダウンロードダイアログを表示させるか選べる。(正確には、Content-Disposition ヘッダを設定する。)

send_file png_image_file_path, :type => 'image/png', :disposition => 'inline' #=> インラインで表示させたり、ブラウザ上で画像を表示させる場合
send_file png_image_file_path, :type => 'image/png' #=> こっちはダウンロードのダイアログが出る

404 とか 403 とかを render するメソッド

render_error 404 などとすると、public/404.html を探してステータスコード 404 をつけて render し、false を返す。

# lib/render_error.rb
module RenderError
  def render_error(status_code)
    render :file => "public/#{status_code.to_s[0..2]}.html", :status => status_code
    return false
  end
end

class ActionController::Base
  include RenderError
end

使い方

# render_error は false を返すので
# filter の返り値にそのまま使うとフィルターチェーンを中断できる
render_error 404

テスト

by メソッド

Restful Authentication や Devise を使ったアプリケーションで、functional test を書く際に、下記のような表記を可能にする。 どちらも認証に使う Model を User と決め打ちしているので、必要に応じて修正する必要がある。

# test/fixtures/users.yml
fixture_user_name:
  email: ...

# ブロック中を users(:fixture_user_name) からのリクエストとしてテスト
by :fixture_user_name do
  get :index
  assert_response :success
end

# lambda を users(:fixture_user_name) からのリクエストとしてテスト
get_index = lambda {
  get :index
  assert_response :success
}
by :fixture_user_name, get_index

Restful Authentication 用

# test/test_helper.rb
class ActiveSupport::TestCase
  def by(fixture_key, proc = nil)
    if fixture_key
      @request.session[:user_id] = users(fixture_key).id
      user_name = fixture_key.to_s.humanize
    else
      user_name = "Anonymous user"
    end

    if proc
      proc.call(user_name)
    else
      yield(user_name)
    end

    @request.session.delete :user_id
  end
end

Devise 用

# test/test_helper.rb
class ActiveSupport::TestCase
  def by(fixture_key, proc = nil)
    if fixture_key
      sign_in :user, users(fixture_key)
      user_name = fixture_key.to_s.humanize
    else
      user_name = "Anonymous user"
    end

    if proc
      proc.call(user_name)
    else
      yield(user_name)
    end

    sign_out users(fixture_key)
  end
end

# もちろん下記も必要
class ActionController::TestCase
  include Devise::TestHelpers
end

関連と fixture

fixture で外部キーの _id を省略すると参照テーブルの fixture ラベルを指定できる。

class Entry < ActiveRecord::Base
  has_many :comments # 必ずしも必要でない
end

class Comment < ActiveRecord::Base
  belongs_to :entry
end
#entries.yml
about_me:
  title: About me
  body: foo bar

#comments.yml
for_about_me:
  entry: about_me
  body: baz

ここで entry と書いてるのは belongs_to :entry と指定したからで、クラス名や外部キー名、テーブル名やフィクスチャのファイル名とは直接関係ない。

たとえば、

class Person < ActiveRecord::Base
  belongs_to :parent, :class_name => 'Person', :foreign_key => :parent_user_id
end

とした場合、

#people.yml
anakin:
  name: Anakin Skywalker

luke:
  name: Luke Skywalker
  parent: anakin

と書ける。

fixture で id を記述しなかった場合の id

fixture で id を記述しなかった場合、自動で id が割り振られるが、この id は下記のコードで生成されている。

# File activerecord/lib/active_record/fixtures.rb
MAX_ID = 2 ** 30 - 1

def self.identify(label)
  Zlib.crc32(label.to_s) % MAX_ID
end

引数の label はフィクスチャのラベル。users(:labocho):labocho である。

Zlib.crc32 は文字列から CRC チェックサム値を生成するメソッドなので、label が同じなら同じ値が返る (いちおう衝突の可能性もある)。

どんな id が割り振られるかは下記のコマンドで確認出来る。

# rails 環境で
ruby script/runner "puts Fixtures.identify(:labocho)" 
# 3.1 以降
rails runner "puts ActiveRecord::Fixtures.identify(:labocho)" 

# ruby だけで
ruby -r zlib -e "puts Zlib.crc32('labocho') % (2 ** 30 - 1)"

file_column のテスト時にはレコードの id をディレクトリ名に使う必要があるが、この方法で割り振られる id を確認しておけば、fixture に id を記述しなくともテストできる。

parallel_tests

parallel_tests はマルチプロセスでテストを行う gem。マルチコア環境では飛躍的にテストが高速化できる。

導入は上記ページにある Readme.md がわかりやすいのでそれを参照。 file_column と組合わせる場合は、少し調整が必要。

設定

environment.rb

gem 名とライブラリ名が異なる場合

gem 名とライブラリ名が異なる場合、config.gem に :lib オプションでライブラリ名を指定しなければならない。これをしていないとサーバ起動時に Missing these required gems: などと怒られる。典型的には gem 名がハイフン入りで、ライブラリ名がスラッシュ入りになるもの。

# config/environment.rb
config.gem "diff-lcs", :lib => "diff/lcs"

Rails 3 の場合は下記の通り

# Gemfile
gem "diff-lcs", :require => "diff/lcs"

database.yml

Access denied for user ‘root’@’localhost’ (using password: YES)

rake db:migrate 時などに、上記のエラーが出た場合、database.yml をチェック。MySQL の場合、user ではなく username とするのが正しいみたい。user になってると無視されて、root としてアクセスしようとする。

production:
  adapter: mysql
  database: database_name
  username: database_user
  password: database_password

データベース

migration を書くときの注意点

migrate / rollback で schema の diff をチェックする

migration を書いたら、migrate して db/schema.rb の diff をチェック (意図した変更が加えられているか)。

rollback してもう一度チェック (diff がないか) する。

ActiveRecord クラスを使わない

既存テーブルの構造を変更する際に、既存のデータを移行するケースがあるが、その際に ActiveRecord クラスを使わない。ある migration の時点で存在しないカラムについて validation などを行うと例外が発生するため。また、クラス名の変更、テーブル名の変更にも弱くなる。面倒だが、execute で生の SQL を発行するのが得策。

データの投入を行わない

初期データの投入は db/seed.rb に定義し、rake db:seed で行う。これは、migration と関係なく変更可能にするため。また、seed.rb では migration とは異なり、ActiveRecord クラスを使っても問題ない。

after を使う

好みだが、カラムの追加時、:after => :foo オプションで、カラムを追加する位置を指定できる。Sequel Pro などで見やすいとか、migrate -> rollback で schema.rb に diff があるのが気持ち悪いのを解消できるとか。その程度のメリットなのでどっちでもいい。

コマンドライン

Rake と script/* での environment の指定

rakeRAILS_ENV=''environment''script/runner-e ''environment''script/console''environment''

# rake
rake db:create RAILS_ENV=production

# script/runner
ruby script/runner -e production "puts 'Hello, Rails!'"

# script/console
ruby script/console production

デプロイ

デプロイまでの手順例

rails 3.0.7 / ruby 1.9.2 on rvm / capistrano / git / passenger 使用。

# local
$ capify .

config/deploy.rb を編集

# http://rvm.beginrescueend.com/integration/capistrano/
$:.unshift(File.expand_path('./lib', ENV['rvm_path'])) # Add RVM's lib directory to the load path.
require "rvm/capistrano"                  # Load RVM's capistrano plugin.
set :rvm_ruby_string, '1.9.2'        # Or whatever env you want it to run in.

# http://d.hatena.ne.jp/kattton/20110121/1295571519
require 'bundler/capistrano'

#---
# Excerpted from "Agile Web Development with Rails, 3rd Ed.",
# published by The Pragmatic Bookshelf.
# Copyrights apply to this code. It may not be used to create training material, 
# courses, books, articles, and the like. Contact us if you are in doubt.
# We make no guarantees that this code is fit for any purpose. 
# Visit http://www.pragmaticprogrammer.com/titles/rails3 for more book information.
#---
# be sure to change these
set :user, '[user]'
set :domain, '[host]'
set :application, '[project]'
set :ssh_options, { :forward_agent => true }

# file paths
set :repository,  "#{user}@#{domain}:git/#{application}.git"
set :deploy_to, "/var/www/#{application}" 

# distribute your applications across servers (the instructions below put them
# all on the same server, defined above as 'domain', adjust as necessary)
role :app, domain
role :web, domain
role :db, domain, :primary => true

# you might need to set this if you aren't seeing password prompts
# default_run_options[:pty] = true

# As Capistrano executes in a non-interactive mode and therefore doesn't cause
# any of your shell profile scripts to be run, the following might be needed
# if (for example) you have locally installed gems or applications.  Note:
# this needs to contain the full values for the variables set, not simply
# the deltas.
# default_environment['PATH']='<your paths>:/usr/local/bin:/usr/bin:/bin'
# default_environment['GEM_PATH']='<your paths>:/usr/lib/ruby/gems/1.8'

# miscellaneous options
set :deploy_via, :remote_cache
set :scm, 'git'
set :branch, 'master'
set :scm_verbose, true
set :use_sudo, false

# task which causes Passenger to initiate a restart
namespace :deploy do
  task :restart do
    run "touch #{current_path}/tmp/restart.txt" 
  end
end

# optional task to reconfigure databases
after "deploy:update_code", :configure_database
desc "copy database.yml into the current release path"
task :configure_database, :roles => :app do
  db_config = "/home/[user]/[project]/config/database.yml"
  run "cp #{db_config} #{release_path}/config/database.yml"
end

ソースの置き場所と database.yml の用意。

# remote
$ sudo mkdir -p /var/www/[project]
$ sudo chown [user]:[group] /var/www/[project]
# remote
$ mkdir -p ~/[project]/config
# database.yml を記述
$ vi ~/[project]/config/database.yml

デプロイ。

# local
# 必要なディレクトリの作成 (初回のみ)
$ cap deploy:setup
# 正常に動作するかチェック
$ cap deploy:check
# デプロイ実行
$ cap deploy:migrations

Passenger の設定。

$ sudo vi /etc/httpd/conf/httpd.conf
# メインで使う Ruby
LoadModule passenger_module /home/[user]/.rvm/gems/ree-1.8.7-2010.02/gems/passenger-3.0.0.pre4/ext/apache2/mod_passenger.so
PassengerRoot /home/[user]/.rvm/gems/ree-1.8.7-2010.02/gems/passenger-3.0.0.pre4
PassengerRuby /home/[user]/.rvm/wrappers/ree-1.8.7-2010.02/ruby

<VirtualHost *:80>
  ServerName [project].penguinlab.jp
  DocumentRoot /var/www/[project]/public
</VirtualHost>

VHost で別の Ruby を使う場合

$ sudo vi /etc/httpd/conf/httpd.conf
# http://blog.phusion.nl/2010/09/21/phusion-passenger-running-multiple-ruby-versions/
<VirtualHost *:80>
  ServerName [project].penguinlab.jp
  DocumentRoot /var/www/[project]/current/public
  PassengerEnabled off
  ProxyPass / http://127.0.0.1:3000/
  ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
# cd /var/www/[project]/current
# passenger start
# だと、Capistrano のバージョン管理に対応せず、restart.txt による再起動もできないので、下記のようにパスを指定する
$ passenger start /var/www/[project]/current -p 3000 -d -e production --pid-file /var/www/[project]/shared/pids/passenger.3000.pid --log-file /var/www/[project]/shared/log/passenger.3000.log

その他

HyperEstraier でノードが追加できない

Rails とは直接関係ないけど、ほかに書くとこないのでここに。

MacPorts でインストールした HyperEstraier を search_do を通して使っているときに、ノードの追加ができない現象が起こった。

Web インターフェイスでノードを追加しようとしても Some error occurred と言われ、追加できない。ログを見ると ERROR DB-ERROR: database problem というエラーが出ている。この後、ノードマスタを再起動しようとしても、最後に追加しようとしたノードを開けずに失敗する (_node にあるノードのディレクトリを 10 未満にすれば再起動できる)。一方 VM 上の CentOS では、問題なく 11 以上のノードが作成できた。

原因はファイルディスクリプタの上限に引っかかっていたため。MacOS X 10.6 では、デフォルトで上限が 256 である。

$ ulimit -n
256

これを適当な大きな値に設定したら、11 以上のノードを作成できるようになった。

$ ulimit -n 1024
1024

%title% ------- なんだかんだでよくできてる1冊。環境構築、チュートリアル、主要部分の解説、デプロイまで、一通り網羅している。初学時はもちろん、慣れてきてからも、いろいろ発見がある。文章も読みやすい。 [%url% %mediumimage%] [%url% %title% / %author% 著. %publisher%, %publishedyear%, %pages%p.]