rakeでmigration

最近の rails はテーブルの作成は、rake の migration というのを使うらしい。
http://www.atmarkit.co.jp/im/carc/serial/proto04/proto04.html
(↑この記事は細かいところが結構間違っている。。)
RakeはRuby Make、つまり、Ruby版のビルドツールで、それを使って ActiveRecord::Migration を利用する。

使い方

ひな形を作って、それを編集する。
以下の例では、membersテーブルを作成するテンプレートを生成。

 $ ./script/generate migration create_members

これを実行すると、以下のような db/migrate/001_create_members.rb ができる。

class CreateMembers < ActiveRecord::Migration
  def self.up
  end

  def self.down
  end
end

このひな型はバージョン管理されていて、ひな形を作るごとに、頭の001が002、003、、と増えていく。self.up には以前のバージョンからの追加分を定義し、self.down には以前のバージョンに戻すときの変更分を定義する。今回は初回なので、まずは create の定義だけでOK。(正確に言うとdownにはdropを定義するんだろうか。。。?)
以下、テーブル作成定義を追加したもの。

class CreateMembers < ActiveRecord::Migration
    def self.up
        create_table :members do |t|
            t.column :id, :integer
            t.column :name, :string
        end
    end

    def self.down
    end
end

編集したら、rakeを使って変更を反映。

$ rake db:migrate

変更を確認。(SQLiteの場合)

$ sqlite3 db/development.sqlite3
sqlite> .schema
CREATE TABLE members (
  "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 
  "name" varchar(255) DEFAULT NULL);
CREATE TABLE schema_info (version integer);
sqlite> .quit

membersテーブルが作成されていることがわかる。(注:上記例は見やすいように改行してある。)

rails 2.0でscaffold

railsのバージョンを2.0にして、sqliteを使うようにしたので、初めから scaffoldで作ってみることにした。が、色々と変更されていて非常にはまった。。。
参考:http://d.hatena.ne.jp/idesaku/20071211/1197386955

これまでだと、データベースにテーブルを作って(migrateから作ったりも含む)、その内容をもとにmodelを生成し、scaffoldを作っていた。しかし、2.0からはいきなり scaffold から作成するように変更された。scaffoldを作成するときにオプションでモデルのフィールドを指定する。従来通りの方法でやろうとすると、scaffoldが作成できなくて泣くはめになる(自分)

以下 2.0での構築方法。
まず、いきなりscaffoldを作成。nameとageという属性をもったMemberモデルを使用。

$ ruby script/generate scaffold Member name:string age:integer
・・・

オプションで与えた属性をもとに、db/migrate/001_create_members.rb が作成される。
中身はこうなっている。

class CreateMembers < ActiveRecord::Migration
  def self.up
    create_table :members do |t|
      t.string :name
      t.integer :age

      t.timestamps
    end
  end

  def self.down
    drop_table :members
  end
end

id属性は特に明示しなくても、デフォルトでつけてくれるようだ。
これをデータベースに反映

$ rake db:migrate

以上で終了。以前に比べてテーブルの作成やモデルの生成手順が減ってすっきりした感がある。新しい方法でやらないと結構はまるので要注意。

Rails 2.0.2でサンプルアプリ

ちょっとましなサンプルを2.0.2の作法で作ってみることにする。
参考:http://www.kestrel.jp/modules/tinyd04//content/index.php?id=1

上記URLを参考に、rails2.0流に。

1.ベースの作成

適当なところで、いつものベースの作成

$ rails rails_2.0-sample
      create
      create  app/controllers
      create  app/helpers
      create  app/models
      create  app/views/layouts
      create  config/environments
      create  config/initializers
・・・
$ cd rails_2.0-sample

2.足場(scaffold)の作成

今回、Person と Person の属する Organazation の二つのモデルを使います。rganazation は名前(name)を持ち、Person は名前(name)と所属するOrganazationのid(orgId)をもちます。それぞれ scaffoldを作成します。

$ ruby script/generate scaffold Organization name:string
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/organizations
      exists  app/views/layouts/
・・・
$ ruby script/generate scaffold Person name:string orgId:integer
      exists  app/models/
      exists  app/controllers/
      exists  app/helpers/
      create  app/views/people
      exists  app/views/layouts/
・・・

scaffold作成の引数に、それぞれ持っている属性を指定します。なお、Person は人の単数形で、テーブル名は自動的に複数形のPeopleに変換されています。
migrateファイルもそれに合わせて自動的に生成されるので、rakeでデータベースへ反映します。

$ rake db:migrate
(in /xxx/xxx/rails_2.0-sample)
== 1 CreateOrganizations: migrating ===========================================
-- create_table(:organizations)
   -> 0.0038s
== 1 CreateOrganizations: migrated (0.0039s) ==================================

== 2 CreatePeople: migrating ==================================================
-- create_table(:people)
   -> 0.0037s
== 2 CreatePeople: migrated (0.0039s) =========================================

3.データベースの確認

一応、sqliteで確認。

$ sqlite3 db/development.sqlite3
SQLite version 3.5.2
Enter ".help" for instructions
sqlite> .schema
 CREATE TABLE organizations (
  "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  "name" varchar(255) DEFAULT NULL,
  "created_at" datetime DEFAULT NULL,
  "updated_at" datetime DEFAULT NULL);
 CREATE TABLE people (
  "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
  "name" varchar(255) DEFAULT NULL,
  "orgId" integer DEFAULT NULL,
  "created_at" datetime DEFAULT NULL,
  "updated_at" datetime DEFAULT NULL);
CREATE TABLE schema_info (version integer);
sqlite>.q

4.サーバ起動と確認

以上で基本的な設定は終了。サーバーを起動して動作確認。

$ ./script/server
=> Booting WEBrick...
=> Rails application started on http://0.0.0.0:3000
=> Ctrl-C to shutdown server; call with --help for options

http://HOSTNAME:3000/organizationhttp://HOSTNAME:3000/people にアクセスするとそれぞれ、組織、人のリストが見れます。

4.表示カラムの追加

peopleの一覧画面をみると、Name と Organization だけ表示されています。ここに、IDと作成日時、更新日時も表示するように修正してみる。app/views/people/index.html.erb を編集する。

<h1>Listing people</h1>

<table>
  <tr>
    <th>id</th>           <!-- ← 追加  -->
    <th>Name</th>
    <th>Orgid</th>
    <th>作成日時</th>     <!-- ← 追加  -->
    <th>更新日時</th>     <!-- ← 追加  -->
  </tr>

<% for person in @people %>
  <tr>
    <td><%=h person.id %></td>           <!-- ← 追加  -->
    <td><%=h person.name %></td>
    <td><%=h person.orgId %></td>
    <td><%=h person.created_at %></td>   <!-- ← 追加  -->
    <td><%=h person.updated_at %></td>   <!-- ← 追加  -->
    <td><%= link_to 'Show', person %></td>
    <td><%= link_to 'Edit', edit_person_path(person) %></td>
    <td><%= link_to 'Destroy', person, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New person', new_person_path %>

これで、再度 peopleのリスト画面を見ると、ID、作成日時、更新日時が表示される。特にサーバの再起動は必要ない。

5.テーブルの結合

people と organization テーブルを結合して、people を表示するときに、orgIdではなく、組織名を表示するようにしてみる。また、新規作成時に所属をidではなく、選択から選べるようにもする。

まず、Person と Organization との関係をモデルに定義する。Person と Organization は 多対1なので、Personモデルに、Organization に所属している belongs_to を定義する。このとき、外部キーとして orgId を使うことを定義。(organization_id とかいうカラム名だったら明示なので不要??)

["app/models/person.rb"]
class Person < ActiveRecord::Base
  belongs_to :organization, :foreign_key => "orgId"
end

Organization のほうに Person を持ってるよという、has_many を定義してもよいが、今回は使わないのでパス。とりあえず、これで、person.organization.name で関連する組織名をとってこれるようになった。
次は、表示部分の変更。app/views/people/index.html.erb を以下のように変更。

<h1>Listing people</h1>

<table>
  <tr>
    <th>id</th>
    <th>Name</th>
    <th>組織</th>              <!-- ← 修正 -->
    <th>作成日時</th>
    <th>更新日時</th>
  </tr>

<% for person in @people %>
  <tr>
    <td><%=h person.id %></td>
    <td><%=h person.name %></td>
    <td><%=h person.organization.name %></td>   <!-- ← 修正 -->
    <td><%=h person.created_at %></td>
    <td><%=h person.updated_at %></td>
    <td><%= link_to 'Show', person %></td>
    <td><%= link_to 'Edit', edit_person_path(person) %></td>
    <td><%= link_to 'Destroy', person, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

orgIdを表示していたところを、ヘッダを「組織名」、値を person.organization.name を表示するように修正。これで、関連する組織名が表示されます。

次は、新規追加時に組織名を選択できるように修正。
app/views/people/new.html.erb を変更。

<h1>New person</h1>

<%= error_messages_for :person %>

<% form_for(@person) do |f| %>
  <p>
    <b>Name</b><br />
    <%= f.text_field :name %>
  </p>

  <p>
    <b>Orgid</b><br />
    <!-- 以下を追加 -->
    <%= select("person", "orgId", Organization.find(:all).collect {|e| [ e.name, e.id ] }) %>
  </p>

  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

<%= link_to 'Back', people_path %>

<%= f.text_field :orgId %> とかあるところを削除して、上記の例の1文に書き換える。同様の修正を app/views/organizations/edit.html.erb に行えば完了。
Webブラウザからアクセスして、新規追加をするときに 組織名を選択できるようになる。