Rails 2.0 をインストールしてみた

チェックポイント

Rails 2.0 (2.0.2) をインストールしてみたので、メモ。

  1. Rails 2.0 をとりあえず使ってみる
  2. コントローラーのコード
  3. JSONPCSRF 対策の相性

情報ソース

このエントリは、実際にやったことをダラダラ書いただけなので、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.

と言われた。いまや RailsRubyGems に完全依存ですか? 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/test1Rails 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 の常套手段だね)

JSONPCSRF 対策の相性

普通に 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:日本語だと「冪等」とかいう小難しい言葉しかないらしく、訳に頭を悩ませている人が多い