Rails 2.0 をインストールしてみた
情報ソース
このエントリは、実際にやったことをダラダラ書いただけなので、Rails 2.0 のさわりを簡潔に知りたければつぎのエントリがお勧め。
Rails 2.0のscaffoldを使ってみた
CSRF 対策がらみでは次のエントリがまとまっている。
Rails 2.0でCSRF対策
(一言くわえておくと、Rails 2.0 ではデフォルトで POST リクエストには認証トークンが必要なようである。したがって、認証トークンを不要にする場合にだけ、protect_from_forgery を使うことになるだろう)
Rails 2.0 をとりあえず使ってみる
RubyGems でごく普通に最新版の Rails をインストール。
sudo gem install rails -y
rails コマンド使用してみる。
$ rails rails2_test create create app/controllers create app/helpers create app/models create app/views/layouts create config/environments create config/initializers create db create doc create lib create lib/tasks create log create public/images create public/javascripts create public/stylesheets create script/performance create script/process create test/fixtures create test/functional create test/integration create test/mocks/development create test/mocks/test create test/unit create vendor create vendor/plugins create tmp/sessions create tmp/sockets create tmp/cache create tmp/pids create Rakefile create README create app/controllers/application.rb create app/helpers/application_helper.rb create test/test_helper.rb create config/database.yml create config/routes.rb create public/.htaccess create config/initializers/inflections.rb create config/initializers/mime_types.rb create config/boot.rb create config/environment.rb create config/environments/production.rb create config/environments/development.rb create config/environments/test.rb create script/about create script/console create script/destroy create script/generate create script/performance/benchmarker create script/performance/profiler create script/performance/request create script/process/reaper create script/process/spawner create script/process/inspector create script/runner create script/server create script/plugin create public/dispatch.rb create public/dispatch.cgi create public/dispatch.fcgi create public/404.html create public/422.html create public/500.html create public/index.html create public/favicon.ico create public/robots.txt create public/images/rails.png create public/javascripts/prototype.js create public/javascripts/effects.js create public/javascripts/dragdrop.js create public/javascripts/controls.js create public/javascripts/application.js create doc/README_FOR_APP create log/server.log create log/production.log create log/development.log create log/test.log
感想:
- config/initializers はまったく新しい
- script/about なんてあったっけ?
- public/.htaccess なんてあったっけ?
- prototype.js が 1.6 になっている。(Rails 1.2 では prototype.js 1.5)
それ以外は Rails 1.2 と共通に見える。
$ script/generate model Entry Rails requires RubyGems >= 0.9.4 (you have 0.9.2). Please `gem update --system` and try again.
と言われた。いまや Rails は RubyGems に完全依存ですか? 1.2 でもそんなことあったかなあ。仕方ないので言われたとおり、
$ gem update --system
してみる。アップグレードすると RubyGems のバージョンは 1.0.1 に上がった。
$ script/generate model Entry exists app/models/ exists test/unit/ exists test/fixtures/ create app/models/entry.rb create test/unit/entry_test.rb create test/fixtures/entries.yml create db/migrate create db/migrate/001_create_entries.rb
出来上がった 001_create_entries.rb の中身。
class CreateEntries < ActiveRecord::Migration def self.up create_table :entries do |t| t.timestamps end end def self.down drop_table :entries end end
おお、t.timestamps って何よ。多分 created_at と updated_at を作るメソッドだとは思うけど。実際 Rails のドキュメントで "And then there‘s TableDefinition#timestamps that‘ll add created_at and updated_at as datetimes." という箇所を見つけた。Rails 1.2 でも migration ファイルを書くのがそれほど面倒だったとは思わないが、さらにタイプ量の節約を試みる DRY (Don't Repeat Yourself) 原則への執念だな。
Rails 2.0 流儀で migration ファイルを書いてみる。
class CreateEntries < ActiveRecord::Migration def self.up create_table :entries do |t| t.string :title, :body t.timestamps end end def self.down drop_table :entries end end
ちなみに Rails 2.0 からデフォルトで SQLite3 を使うようになっているらしい。(config/database.yml 参照)
$ rake db:migrate
で Rails 1.2 時代と同様テーブルが出来たっぽい。
$ sqlite3 development.sqlite3 SQLite version 3.3.8 Enter ".help" for instructions sqlite> .schema CREATE TABLE entries ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "title" varchar(255) DEFAULT NULL, "body" varchar(255) DEFAULT NULL, "created_at" datetime DEFAULT NULL, "updated_at" datetime DEFAULT NULL); CREATE TABLE schema_info (version integer);
確かに title, body, created_at, updated_at の4つのカラムが期待したとおりに出来ている。
ちなみにデータベーススキーマを Rails 形式で表示する db/schema.rb はどうなっているだろうか?
ActiveRecord::Schema.define(:version => 1) do create_table "entries", :force => true do |t| t.string "title" t.string "body" t.datetime "created_at" t.datetime "updated_at" end end
t.timestamps が created_at と updated_at の定義に変わっていることが確認できる。
準備ができたので scaffold でアプリケーションを作成してみる。
$ script/generate scaffold Entry exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/entries exists app/views/layouts/ exists test/functional/ exists test/unit/ create app/views/entries/index.html.erb create app/views/entries/show.html.erb create app/views/entries/new.html.erb create app/views/entries/edit.html.erb create app/views/layouts/entries.html.erb create public/stylesheets/scaffold.css dependency model exists app/models/ exists test/unit/ exists test/fixtures/ identical app/models/entry.rb identical test/unit/entry_test.rb identical test/fixtures/entries.yml exists db/migrate Another migration is already named create_entries: db/migrate/001_create_entries.rb
できてますね。なんちゃら.html.erb というファイルが views の下に。あとは生成されたファイルで Rails 1.2 時代と大きく変わるものはなさそうだ。
・・・ってお前の目は節穴か。実は Rails 2.0 から scaffold が migration ファイルも含めて生成するように変更されているらしい。これはよい変化だ。本来こうあるべきだと思う。
気を取り直してやりなおし。上で作ったファイルは全部消し、rails コマンドでアプリケーションの雛形を作り直した。
$ script/generate scaffold Entry title:string body:string exists app/models/ exists app/controllers/ exists app/helpers/ create app/views/entries exists app/views/layouts/ exists test/functional/ exists test/unit/ create app/views/entries/index.html.erb create app/views/entries/show.html.erb create app/views/entries/new.html.erb create app/views/entries/edit.html.erb create app/views/layouts/entries.html.erb create public/stylesheets/scaffold.css dependency model exists app/models/ exists test/unit/ exists test/fixtures/ create app/models/entry.rb create test/unit/entry_test.rb create test/fixtures/entries.yml create db/migrate create db/migrate/001_create_entries.rb create app/controllers/entries_controller.rb create test/functional/entries_controller_test.rb create app/helpers/entries_helper.rb route map.resources :entries
今度はちゃんと entries_controller.rb のファイルができている。
$ rake db:migrate
データベースにテーブルも作られた。
$ script/server
Rails アプリ起動。うまく起動できたので、ブラウザからアクセス(http://example.com:3000/entries/)してみると・・・
CGI::Session::CookieStore::TamperedWithCookie in EntriesController#index
といわれてしまった。Rails 2.0 からセッションは cookie に保存されるようになったと聞いたが、その関係か。とりあえず example.com への cookie を削除してみる。
おお、今度はきちんと画面が出てきた。画面下部の "New Entry" をクリックして、新規登録画面へ遷移する。そのページのソースの一部は以下の通り。
<form action="/entries" class="new_entry" id="new_entry" method="post"><div style="margin:0;padding:0"><input name="authenticity_token" type="hidden" value="08fca19ded408a43641c951450fb284a5600a15d" /></div> <p> <b>Title</b><br /> <input id="entry_title" name="entry[title]" size="30" type="text" /> </p> <p> <b>Body</b><br /> <input id="entry_body" name="entry[body]" size="30" type="text" /> </p> <p> <input id="entry_submit" name="commit" type="submit" value="Create" /> </p> </form>
authenticity_token というやつが Rails 2.0 で新しく登場した CSRF 対策用の認証トークンだ。これはワンタイムトークンではなく、セッション間ずっと有効なセッショントークンである。
実験してみたが、同一セッション中はトークンは変化しないが、ブラウザを再起動する(=別のセッションに入る)と変化する。
コントローラーのコード
コントローラーのコードがあまりに変わっていて愕然とした。たとえばこんな感じ。
class EntriesController < ApplicationController # GET /entries # GET /entries.xml def index @entries = Entry.find(:all) respond_to do |format| format.html # index.html.erb format.xml { render :xml => @entries } end end # GET /entries/1 # GET /entries/1.xml def show @entry = Entry.find(params[:id]) respond_to do |format| format.html # show.html.erb format.xml { render :xml => @entry } end end # GET /entries/new # GET /entries/new.xml def new @entry = Entry.new respond_to do |format| format.html # new.html.erb format.xml { render :xml => @entry } end end # GET /entries/1/edit def edit @entry = Entry.find(params[:id]) end # POST /entries # POST /entries.xml def create @entry = Entry.new(params[:entry]) respond_to do |format| if @entry.save flash[:notice] = 'Entry was successfully created.' format.html { redirect_to(@entry) } format.xml { render :xml => @entry, :status => :created, :location => @entry } else format.html { render :action => "new" } format.xml { render :xml => @entry.errors, :status => :unprocessable_entity } end end end # PUT /entries/1 # PUT /entries/1.xml def update @entry = Entry.find(params[:id]) respond_to do |format| if @entry.update_attributes(params[:entry]) flash[:notice] = 'Entry was successfully updated.' format.html { redirect_to(@entry) } format.xml { head :ok } else format.html { render :action => "edit" } format.xml { render :xml => @entry.errors, :status => :unprocessable_entity } end end end # DELETE /entries/1 # DELETE /entries/1.xml def destroy @entry = Entry.find(params[:id]) @entry.destroy respond_to do |format| format.html { redirect_to(entries_url) } format.xml { head :ok } end end end
Rails 1.2 と全然ちがう。しかも、上の EntriesController クラスに
def test1 render :text => "abcdef" end
というメソッドを追加して、http://example.com:3000/entries/test1 と Rails 1.2 風にアクセスしようとすると、
ActiveRecord::RecordNotFound in EntriesController#show Couldn't find Entry with ID=test1
といわれてエラーになる。なんで EntriesController#show が関係あるんだ?ルーティングの問題か?と思って、
config/routes.rb をみると、始めのほうに、
map.resources :entries
という箇所がある。どうやらここでルーティングの設定がおこなわれているらしい。ドキュメントを調べてみた。
これは要するに、たとえば MessagesController に対して
Named Route Helpers ============ ===================================================== messages messages_url, hash_for_messages_url, messages_path, hash_for_messages_path message message_url(id), hash_for_message_url(id), message_path(id), hash_for_message_path(id) new_message new_message_url, hash_for_new_message_url, new_message_path, hash_for_new_message_path edit_message edit_message_url(id), hash_for_edit_message_url(id), edit_message_path(id), hash_for_edit_message_path(id)
という感じの名前つきルート(named route) とヘルパメソッドを生成してくれるらしい。Rails 1.2 のようにコントローラにアクションを追加するには、次のようにする。
map.resources :entries, :collection => { :test1 => :get }
これで、http://example.com:3000/entries/test1/ という URL を GET で取得することが可能になる。また、
map.resources :entries, :member => { :test1 => :get }
これで、http://example.com:3000/entries/1/test1/ という URL を GET で取得することが可能になる。このとき params[:id] には entries/1 の 1 が入ることになる。
つまり、map.resources を使う限り、いままでみたいにコントローラクラスに public なメソッドを追加したら、ただちにアクションになる、という軽いノリはなくなったことになる。(もちろん map.resources をつかわなければ従来どおりだ) 縛りがきつくなったが、この縛りに慣れれば、プログラミングが楽になりそうだ。(Convention over Configuration, Rails の常套手段だね)
JSONP と CSRF 対策の相性
普通に javascript 取得の GET リクエストが成功する。別に token がなくちゃ駄目、ということはないらしい。普通の GET に関しては。ドキュメント ActionController::RequestForgeryProtection::ClassMethods#protect_from_forgery の項目によれば、"Also, GET requests are not protected as these should be indempotent anyway." (それに、GET リクエストは保護されないよ。いずれにしろ、GET は idempotent だろ) とのこと。idempotent とは、ごく簡単に言えば、「ある対象になんらかの操作を施しても、対象が変化しない性質」ということらしい。*1
実際には GET でサーバの状態を恒久的に変化させる操作(例:データベースへの書き込み)も、サーバ側のプログラムの作り方如何では可能だが、そんなアプリは REST の思想に反するから、保護しないということらしい。さすが Rails。DHH が opinionated software(主張を持ったソフトウェア) と呼ぶだけのことはある。
*1:日本語だと「冪等」とかいう小難しい言葉しかないらしく、訳に頭を悩ませている人が多い。