ログインしてさらにmixiを楽しもう

コメントを投稿して情報交換!
更新通知を受け取って、最新情報をゲット!

Ruby勉強会@広島コミュのA Many-to-Many tutorial for Rails

  • mixiチェック
  • このエントリーをはてなブックマークに追加
A Many-to-Many tutorial for Rails
                by jeffrey Hacks

(http://jrhicks.net/Projects/rails/has_many_and_belongs_to_many.pdf)

イントロダクション:
この簡単なチュートリアルは初めから終わりまで多対多のリレーションシップで要求されるモデル、ビュー、コントローラーの例です。

これはファイナンスアプリケーションのチュートリアルですのでrailsアプリケーションをfinanceで作成して、database.ymlファイルを設定し、サーバを起動してください。

1.モデル
例題のアプリケーションは財務の経費とtags(tagってどう訳すの?)をモデル化してます。railsをデフォルトで動作させる為、データベースのテーブル名とフィールドの厳格な命名規約に従います。

命名規約:
・expensesはexpenseの複数系です。
・tagsはtagの複数系です。
・どちらのテーブルのプライマリ・キーも小文字のidを使います。
・結合テーブルはアルファベット順にexpenses_tagsというテーブル名になります。
・tagのidフィールドに関連付けられたフィールドはtas_idです
・expenseのidフィールドに関連付けられたフィールドはexpense_idです

[翻訳者(注) データベース作成を追加]-------------------------------

データベースの作成:

$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 362 to server version: 4.1.11-Debian_4sarge2-log

mysql> create database many_to_many;
Query OK, 1 row affected (0.00 sec)


mysql> grant all on many_to_many.* to 'kajihen'@'localhost';
Query OK, 0 rows affected (0.00 sec)

# config/database.ymlも編集してください!
----------------------------------------------------------

次のスキーマーを使ってデータベースを作成してください。

CREATE TABLE expenses (
id int(11) NOT NULL auto_increment,
amount float NOT NULL default 0,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE tags (
id int(11) NOT NULL auto_increment,
name varchar(100) NOT NULL default "",
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE expenses_tags (
expense_id int(11) NOT NULL default '0',
tag_id int(11) NOT NULL default '0'
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


expenseとtagモデルの為のscaffoldを生成します。

$ ruby script/generate scaffold expense

expense.rbファイルを編集してexpenseモデルにhas_and_belongs_to_many :tagを記述してください。

2.View
Webにアクセスするユーザにexpenseを多くのtagsに関連付けるために、我々はチェックボックスを使います。これは次のようなページになります。

(図を参照)

フォームは動的にデータベースのtagsがら生成されます。データベースにデータを入れるために次のSQLを実行してください

$ mysql many_to_many
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 369 to server version: 4.1.11-Debian_4sarge2-log

Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> insert into tags(name) values('food');
Query OK, 1 row affected (0.00 sec)

mysql> insert into tags(name) values('restaurant');
Query OK, 1 row affected (0.00 sec)

mysql> insert into tags(name) values('lodging');
Query OK, 1 row affected (0.00 sec)

ビューはtagsをロードするexpenses_controllerに依存しています。(17行と32秒に重複してますが)newアクションとeditアクションの両方に@tags = Tag.find_all 行を追加してしてください。

それでは実際にedit.rhtmlとnew.rhtmlをカスタマイズしましょう。我々はひとつの場所でこれを行います。expensesディレクトリにある_form.rhmlを編集して7行目から13行目までのコードが含まれるようにします。

 1 <%= error_messages_for 'expense' %>
 2
 3 <!--[form:expense]-->
 4 <p><label for="expense_amount">Amount</label><br/>
 5 <%= text_field 'expense', 'amount' %>
 6 <br />
 7 <% for tag in @tags %>
 8  <input type="checkbox"
 9     id="<%= tag.id %>"
10     name="tag_ids[]"
11     value="<%= tag.id %>">
12   <%= tag.name %>
13 <% end %>
14 </p>
15
16 <!--[eoform:expense]-->

expensesにあるtagsは編集時にはチェックされるべきです。これを可能にする為に12行目に次のif文を追加します。

 1 <%= error_messages_for 'expense' %>
 2
 3 <!--[form:expense]-->
 4 <p><label for="expense_amount">Amount</label><br/>
 5 <%= text_field 'expense', 'amount' %>
 6 <br />
 7 <% for tag in @tags %>
 8  <input type="checkbox"
 9     id="<%= tag.id %>"
10     name="tag_ids[]"
11     value="<%= tag.id %>"
12     <% if @expense.tags.include? tag %>checked="checked"<% end %>
13   ><%= tag.name %>
14 <% end %>
15 </p>
16
17 <!--[eoform:expense]-->
18

tagsをビューで利用可能にするためにlistビューも編集します。list.rhtmlファイルの8行目と17行目〜19行目にコードを追加します。

 1 <h1>Listing expenses</h1>
 2
 3 <table>
 4  <tr>
 5  <% for column in Expense.content_columns %>
 6   <th><%= column.human_name %></th>
 7  <% end %>
 8  <th>tags</th>
 9  </tr>
10
11 <% for expense in @expenses %>
12  <tr>
13  <% for column in Expense.content_columns %>
14   <td><%=h expense.send(column.name) %></td>
15  <% end %>
16   <td>
17    <% for tag in expense.tags %>
18     <%= tag.name %>
19    <% end %>
20   </td>
21   <td><%= link_to 'Show', :action => 'show', :id => expense %></td>
22   <td><%= link_to 'Edit', :action => 'edit', :id => expense %></td>
23   <td><%= link_to 'Destroy', { :action => 'destroy', :id => expense }, :confirm => 'Are you sure?' %></td>
24  </tr>
25 <% end %>
26 </table>
27
28 <%= link_to 'Previous page', { :page => @expense_pages.current.previous } if @expense_pages.current.previous %>
29 <%= link_to 'Next page', { :page => @expense_pages.current.next } if @expense_pages.current.next %>
30
31 <br />
32
33 <%= link_to 'New expense', :action => 'new' %>

3.コントローラー
expense_controllerを更新してeditとnewビューからのリクエストを受け取るメソッドを作成します。関連を保持するためにTag.find(@params[:tag_ids] if @params[:tag_ids]のようにtag_idsを実際のTagオブジェクトに変換する必要があります。

if @params[:tag_ids]の部分はもしユーザがtagをSELECTできないときにnilオブジェクトによりエラーを回避させてます。

expenses_controller.rbの32行と37行に追加します。

 1 class ExpensesController < ApplicationController
 2  def index
 3   list
 4   render :action => 'list'
 5  end
 6
 7  def list
 8   @expense_pages, @expenses = paginate :expenses, :per_page => 10
 9  end
10
11  def show
12   @expense = Expense.find(params[:id])
13  end
14
15  def new
16   @expense = Expense.new
17   @tags = Tag.find(:all)
18  end
19
20  def create
21   @expense = Expense.new(params[:expense])
22   if @expense.save
23    flash[:notice] = 'Expense was successfully created.'
24    redirect_to :action => 'list'
25   else
26    render :action => 'new'
27   end
28  end
29
30  def edit
31   @expense = Expense.find(params[:id])
32   @tags = Tag.find(:all)
33  end
34
35  def update
36   @expense = Expense.find(params[:id])
37   @expense.tags = Tag.find(@params[:tag_ids]) if @params[:tag_ids]
38   if @expense.update_attributes(params[:expense])
39    flash[:notice] = 'Expense was successfully updated.'
40    redirect_to :action => 'show', :id => @expense
41   else
42    render :action => 'edit'
43   end
44  end
45
46  def destroy
47   Expense.find(params[:id]).destroy
48   redirect_to :action => 'list'
49  end
50 end



オプション
もしあなたが意図していることがユーザがtagを選択することを強制したいのでしたら、expenseモデルにこの文を追加するのをお勧めします

$ vi app/models/expense.rb

 1 class Expense < ActiveRecord::Base
 2  has_and_belongs_to_many :tags
 3
 4  def validate
 5   if tags.blank?
 6    errors.add_to_base('You must specify a tag')
 7   end
 8  end
 9 end

結果:

複数のtagsを持つexpenseを追加します。

(図を参照)

createボタンをした後にtagsを格納したビューです

(図を参照)

チェックされ格納されたtagsを見るためにexpenseのnewコントローラで編集します

(図を参照)

expenses_tagsテーブルのエンティティを見てみましょう!

(図を参照)

謝辞:
Sheldon Hearm, Ecow, Spirails, Brian NG, Brandt(敬称略)みんなアリガトー

翻訳者(注): 謝辞おもいっきりはしょってます(笑)

コメント(0)

mixiユーザー
ログインしてコメントしよう!

Ruby勉強会@広島 更新情報

Ruby勉強会@広島のメンバーはこんなコミュニティにも参加しています

星印の数は、共通して参加しているメンバーが多いほど増えます。