Rails tips
提供: ペンギンラボ Wiki
明示してなければ 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.method か request.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' #=> こっちはダウンロードのダイアログが出る
- RFC 1806 - Internet Engineering Task Force Content-Disposition ヘッダについて
- RFC 1806: The Content-Disposition Header 同日本語訳
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 の指定
rake は RAILS_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
本
RailsによるアジャイルWebアプリケーション開発 第3版
なんだかんだでよくできてる1冊。環境構築、チュートリアル、主要部分の解説、デプロイまで、一通り網羅している。初学時はもちろん、慣れてきてからも、いろいろ発見がある。文章も読みやすい。
RailsによるアジャイルWebアプリケーション開発 第3版 / Sam Ruby 著. オーム社, 2009, 675p.
