WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

Ruby on Rails : テーブル間リレーションシップ

やっと Ruby on Rails ネタの続きが書けました。もっさりした進み具合で申し訳ありません。やはり試行錯誤が入るとなかなか手際よく進めることができませんね。本日は、Ruby on Rails でテーブル間リレーションシップを扱う方法をご紹介しようと思います。詳細な説明は次回に譲るとして、本日は articles テーブルと categories テーブルとの間にリレーションを設定し、article の編集画面にカテゴリの指定を追加する作業手順をご紹介します。

これまでの例に漏れず、テーブル間リレーションシップに関しても Active Record の優れたO/Rマッピングによって非常に素直な形で実装できます。必要なのはテーブルのスキーマ変更とモデルクラスへのたった 1 行の追加だけ。それだけで参照先テーブルのデータが自動的に読み込まれ、参照元モデルクラスのインスタンス変数として直接アクセスできるようになります。また、リレーションの形式も単純な 1 対 1 の関係だけではなく、1 対多や多対多のリレーションも扱うことができます。本日ご紹介する例ではひとつの Category に複数の Article が所属することになりますから、1 対多のリレーションを使うことになりますね。

それでは、具体的な手順をみていくことにしましょう。今回もプロジェクトディレクトリは "~/blognavi"、サイト URL は "http://localhost:3000/" を前提にします。

categories テーブルの Scaffold を作成する

テーブル間リレーションの作業に入る前に、やり残しを片付けてしまいましょう。これまでの作業でカテゴリーのテーブルは作成しましたが、データ入力用のインターフェースがありませんでした。これではカテゴリーが作成できないので、Scaffold で作ってしまいましょう。以下のコマンドを実行してください。

cd ~/blognavi
ruby script/generate scaffold Category Category

これで一連のカテゴリー管理ページが作成されます(Scaffold についてはこちらの記事を参照してください)。サーバーを起動して "http://localhost:3000/category" にアクセスし、実際にカテゴリーをいくつか入力してみてください。

テーブルに参照先 id のカラムを追加する

カテゴリーを操作できるようになったので、本題であるテーブル間リレーションの作成に入りましょう。まずは articles テーブルにカテゴリー ID のカラムを追加します。ここでも例に漏れず命名規則が決まっており、テーブル間リレーションの ID を格納するカラムは "<単数形の参照先テーブル名>_id" とすることになっています。今回の場合、参照先テーブルは "categories" ですから、それを参照するカラムは "category_id" ですね。こうしておけば余計なオプションなどを指定する必要がなくなります。

migration を作成する

データベースの操作には migration を利用すると便利です(migration に関してはこちらの記事を参照してください)。まずは以下のコマンドを実行して、新しい migration を作成します。

ruby script/generate migration AddCategoryIdToArticles

"db/migrate/002_add_category_id_to_articles.rb" というファイルが作成されるので、その内容を以下のように変更します。処理内容としては、articles テーブルに INTEGER 型の category_id カラムを追加し、そのカラムのインデックスを作成する、という感じです。ちなみにインデックスとは検索を効率よく行うためのデータで、これをあらかじめ作成しておくことで、そのカラムをキーにしたクエリーを効率よく実行できます。 テーブル間リレーションシップに使用する "*_id" カラムには必ず作成すると考えておいたほうが良いでしょう。

class AddCategoryIdToArticles < ActiveRecord::migration
  def self.up
    add_column(:articles, :category_id, :integer)
    add_index(:articles, :category_id)
  end
 
  def self.down
    remove_index(:articles, :column => :category_id)
    remove_column(:articles, :category_id)
  end
end

self.down の記述は self.up による変更を元に戻す(ロールバック)処理です。これを記述することで、いつでもデータベーススキーマを任意の時点の状態に戻すことができます。 migration の強力な機能のひとつです。

migration を実行する

migration が定義できたので、さっそくデータベースに適用しましょう。以下のコマンドを実行すれば、migration が実行されてカラムとインデックスが追加されます。

cd ~/blognavi
rake migrate

mysql コマンドなどでカラムが正常に追加されていることを確認してください。

外部キーについて

製作中の blognavi ではすべてのテーブルを MyISAM 形式で統一していますので、category_id カラムは通常の INTEGER 型カラムとして作成しました。しかし、本格的なアプリケーションではテーブルを InnoDB 形式として、きちんと外部キー制約をかけるのが理想です。そうすることで、リレーションの整合性をデータベースが保証してくれます。外部キーについてはこちらのページが詳しいです。

しかし、現在のところ外部キー関連の操作は SQL を直接記述しなければならず、その他にも細かい制約があるため、けっこう手間がかかります。また、 SQLite など、そもそも外部キーをサポートしていないデータベースもあります。外部キー制約をかけるかどうかは、開発にかけられる工数や利用可能な環境などを考慮して決定する必要があるでしょう。

いずれにせよ、 MySQL なら途中で InnoDB 形式に移行することも可能なので、開発段階ではさほど神経質にならなくても大丈夫だと思います。

モデルにリレーションの指定を行う

どのカラムが参照先テーブルの ID であるかは命名規則から識別できますが、それがどのような形式のリレーションであるかまではわかりません。その情報を Ruby on Rails に伝えるために、モデルクラスに以下の宣言を記述します。どの宣言を使うかによって、追加されるメソッドも微妙に違ってきます。

宣言 適用対象
belongs_to 1対1、もしくは 1対多の関係で、参照先 ID を持っている側
has_one 1対1 の関係で、参照先 ID を持たない側
has_many 1対多の関係で、「多」の側(参照先 ID を持っていない)
has_many_and_belongs_to 多対多の関係(双方で共通)

これらは、クラス定義の先頭に "<宣言> :<参照先テーブル名>" という形式で記述します。ややこしいのが参照先テーブル名の指定で、 belongs_tohas_one では単数形、 has_manyhas_many_and_belongs_to では複数形で記述しなければなりません。相手が複数になるならテーブル名も複数、というわけです。うぅ、これは勘弁してほしい・・・英語漬けよりも英語漬けな感じですね(TДT;

ともあれ、blognavi ではカテゴリーが複数の記事を所有しているわけですから、以下のような記述になります。

app/models/article.rb

class Article < ActiveRecord::Base
  belongs_to :category
end

app/models/category.rb

class Category < ActiveRecord::Base
  has_many :articles
end

これで、Ruby on Rails がリレーションを正しく処理できるようになりました。

ユーザーインターフェースを整える

最後に、記事の作成・編集・詳細ページにカテゴリーの表示・変更の機能を追加しましょう。Scaffold が生成したコードに若干手を加えて実装します。

ビューの変更

まずはビューから変更しましょうか。"app/views/article" ディレクトリ内にそれぞれのアクションに対応する ".rhtml" ファイルが格納されています。そのうち "_form.rhtml" をエディタに読み込み、赤字の部分を追加してください。この "_form.rhtml" は "new.rhtml" と "edit.rhtml" で共有される、フォーム部分を定義した部分テンプレートです。部分テンプレートに関しては次回で詳細をご紹介する予定です。

<%= error_messages_for 'article' %>

<!--[form:article]-->
<p><label for="article_number">Number</label><br/>
<%= text_field 'article', 'number'  %></p>

<p><label for="article_title">Title</label><br/>
<%= text_field 'article', 'title'  %></p>

<p><label for="article_category_id">Category</label><br/>
<%= collection_select(:article, :category_id, @categories, :id, :name) %></p>
<!--[eoform:article]-->

追加した collection_select はドロップダウンのリストボックスを作成するヘルパーメソッドです。上記の例では、 "@categories[n].name" をキャプション、 "@categories[n].id" を value 属性としてリストボックスの項目を追加し、 "@article.cagetory_id" をデフォルトで選択状態にします。詳細は後々の記事でご紹介しようと思いますので、ここでは割愛させていただきます。

さらに、詳細表示用の "show.rhtml" にカテゴリーの表示を追加します。7 行目からの 3 行が追加部分です。

<% for column in Article.content_columns %>
<p>
  <b><%= column.human_name %>:</b> <%=h @article.send(column.name) %>
</p>
<% end %>

<% if @category_name %>
  <p><b>category</b> <%=h @category_name %></p>
<% end %>

<%= link_to 'Edit', :action => 'edit', :id => @article %> |
<%= link_to 'Back', :action => 'list' %>

"@category_name" が偽でなければカテゴリー名を表示する、という感じです。 "h" もヘルパーメソッドで、HTML の特殊文字をエスケープするためのものです。

コントローラの変更

ビューに追加したコードで参照しているインスタンス変数を設定するため、 ArticleController クラスに処理を追加します。ファイルは "app/controllers/article_controller.rb" ですね。ここでも追加部分を赤字で示してあります。このクラスはちょっとコード量が多いので、途中は省略しますね。

class ArticleController < ApplicationController

  #...中略...

  def show
    @article = Article.find(params[:id])
    if @article.category
      @category_name = @article.category.name
    end
  end

  def new
    @article = Article.new
    generate_category_list
  end

  #...中略...

  def edit
    @article = Article.find(params[:id])
    generate_category_list
  end

  #...中略...

private

  def generate_category_list
    @categories = Category.find(:all, :order => "name")
  end
end

カテゴリーの名前が "@article.category.name" で参照できているところがポイントです。 belongs_to 宣言をモデルクラスに追加すると、このように "<単数形のテーブル名>.<カラム名>" で参照先テーブルのデータにアクセスできます。その他にもいくつかのメソッドが追加されますが、それらは後々ご紹介していきます。

また、 generate_category_list メソッドで使用している find メソッドも初出ですね。 find はすべてのモデルクラスが持つメソッドで、 SQLSELECT を発行してテーブルからデータを読み込むために使用します。上記では、 categories テーブルのすべての行を名前( name カラム)順に取得しています。

最低限ではありますが、以上でそれぞれの記事にカテゴリーを設定できるようになりました。ファイル全体を掲載したためちょっと長ったらしくなっていますが、追加したコードはごく僅かなのがお分かりいただけると思います。

使ってみる

それでは、実際にカテゴリーを設定してみましょう。まずは以下のようにしてサーバーを起動します。

cd ~/blognavi
ruby script/server

そして、Web ブラウザで "http://localhost:3000/article" をアクセスし、適当な記事を編集しましょう(記事がなければ新規作成でも OK です)。以下のようにカテゴリーを選択するドロップダウンが表示されるはずです。当然カテゴリーがなければなにも表示されないので、あらかじめ "http://localhost:3000/category" のページで追加しておいてください。

blognavi カテゴリ指定の付いた記事編集画面

そこで適当にカテゴリーを選択して保存すれば、詳細表示の画面で以下のように確認できるはずです。

blognavi カテゴリー指定の付いた記事詳細画面

本日は、Ruby on Rails を使って複数のテーブル間でリレーションを作る方法を、記事とカテゴリーを例にご紹介しました。テーブル間リレーションシップというとなにやら難しそうですが、 Ruby on Rails ならいとも簡単に実現できることがお分かりいただけたかと思います。しかし、本日ご紹介したのは Ruby on Rails のリレーションシップ関連機能のごく一部です。今後も機会をみてさらなる詳細をご紹介するつもりですので、ご期待ください!(^^)

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles