速習Larave3(その14)、タグNo.1

タグ: Laravel3  

このシリーズ最後のチュートリアルです。ミニブログという割には、高機能になってしまいましたからね。最後はタグの追加です。これで多:多の関係を学びます。

今回は、タグを導入します。

タグを考えてみましょう。ある記事に付けるカテゴリーのことです。例えば「飼い犬の散歩」というタイトルの記事には「犬」、「散歩」、「しつけ」というタグが考えられます。

使用者は興味のあるタグで、読みたい記事を見つけることができます。分かりますよね?なにせ、このサイトでも使っていますからね。

ある記事は複数のタグを持つことができます。「一つの記事は複数のタグを所有する」ですね。では、1:多の関係でしょうか。

逆に考えてみましょう。タグは一つの記事に限定されていません。「犬」のタグは「犬の選び方」、「犬をしつける」、「美味しい赤犬の食べ方」などの記事に付けられる可能性があります。すると「一つのタグは複数の記事に所属する」なんでしょうか。

そう定義してもいいんですが、一方的に片方が親で、残りが子という関係ではなく、対等ですから、「一つの記事は複数のタグを所有しつつ、そのタグにも所属する」、「一つのタグは複数の記事を所有しつつ、そのタグにも所属する」と考えます。言葉を入れ替えれば、二つとも同じ関係です。

この関係を記事テーブルと、タグテーブルだけで表現しようとすると、結構面倒です。(興味のある方はチャレンジしてみてください。何も考えなければすーっと組めますが、それでは障害が発生するでしょう。二つのテーブルに対し、複数のレコードに対する操作を安全に同調して行うためには、テーブル単位でのロックとか、トランザクションとか、色々対応策を考えなければなりません。)

2つだけで実現しようとすると面倒ですが、ここに両方の関係を表すだけの中間テーブルを導入すると随分簡単になります。

まずはテーブルを考えてみましょう。説明に必要なカラムのみ表記します。

postsテーブル
カラム名 説明
id 記事ID
title タイトル
body 本文
tags テーブル
カラム名 説明
id タグID
name タグ名称
post_tag テーブル
カラム名 説明
id 主キー
post_id 記事ID
tag_id タグID

post_tagテーブルのidだけでなく、postsテーブルでもtagsテーブルでもidは使用されており、どちらも主キーです。Eloquentを使用する時には必ず主キーに符号なし整数のカラムが必要で、それはpost_tagのような中間テーブルでも同様です。

post_tagテーブルのidは直接操作することはまず無いでしょうが、Laravelが内部で利用しているため、存在していないとSQLエラーが発生してしまいます。

中間テーブルはどのポストとどのタグを結びつけるかだけを表現します。そのため、二つのテーブルのIDを持っているだけの、シンプルな作りです。IDが3の記事にIDが4のタグを付け加えるには、post_idが3,tag_idが4の記事をインサートするだけです。ある記事に対するタグを削除する場合もレコードのデリートで対応できます。ある記事のタグを知りたければ記事のIDでセレクトします。あるタグに含まれる記事を知りたければそのタグのIDでセレクトします。全てがひとつのテーブルのSQL操作で事足ります。

何を行うのかを理解できたら、今度はLaravelのEloquentがこの多:多関係をどう提供しているのか、学んで行きましょう。

テーブル作成

まずはtagsテーブルと、中間テーブルのpost_tagを作成しましょう。

マイグレーションを生成し、その内容を以下のように更新、それからマイグレーションを実行してください。

<?php

class Create_Tags_Table {

    /**
     * Make changes to the database.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tags',
            function($table) {
                $table->increments('id');
                $table->string('name', 20);
                $table->timestamps();
            });
    }

    /**
     * Revert the changes to the database.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('tags');
    }

}

tagsテーブルの生成に関しては、新しいものはありません。今までの応用です。

では中間テーブルを作成しましょう。再度、マイグレーション作成、変更、実行を行なってください。

<?php

class Create_Post_Tag_Table
{

    /**
     * Make changes to the database.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('post_tag',
            function($table) {
                $table->increments('id');
                $table->integer('post_id')
                    ->unsigned();
                $table->integer('tag_id')
                    ->unsigned();
                $table->timestamps();
            });
        // ユニークキーと外部キー
        Schema::table('post_tag',
            function($table) {
                $table->unique(array('post_id', 'tag_id'));
                $table
                    ->foreign('post_id')->references('id')->on('posts')
                    ->on_update('cascade')->on_delete('cascade');
                $table
                    ->foreign('tag_id')->references('id')->on('tags')
                    ->on_update('cascade')->on_delete('cascade');
            });
    }

    /**
     * Revert the changes to the database.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('post_tag',
            function($table) {
                $table->drop_foreign('post_tag_tag_id_foreign');
                $table->drop_foreign('post_tag_post_id_foreign');
                // ユニークキーはテーブルと一緒に削除できるので
                // 本当は無理に書かなくても良い。
                $table->drop_unique('post_tag_post_id_tag_id_unique');
            });
        Schema::drop('post_tag');
    }

}

中間テーブルの名前はテーブルの単数形をアルファベット順に並べたものが規約です。これで後はLaravelが面倒を見てくれるというわけです。

今回、新しいのはユニークキーの指定です。ユニークキーを2つ指定しています。複合キーというやつですよ。配列で指定します。

ユニークキーを指定しなくても、Laravelが上手くやってくれますが、時には自分で操作する場合もあります。そんな時にレコードの一意性を失わないために指定しています。(ユニークキーは完全に一意性が保証されるわけでありません。null項目に関して注意するべき点があります。ただし、通常のカラム生成ではnullを許さないのがデフォルトです。post_tagテーブルでも、許していませんから、null値を保存しようとするとエラーになってくれます。)

ちなみに外部束縛キーを指定してるのは、もちろんそのキーが他のテーブルのIDに存在していることを確実にするためです。そして削除と更新時の処理を指定し、関係付けがプログラムミスで壊れないように指定しています。

->on_update('cascade')は親のキー値が変更された場合、それに紐付いている子の対応するカラムの値を変更してくれる設定です。

->on_delete('cascade')は親が削除された場合に、それに紐付いている子のレコードも一緒に削除する設定です。

->on_update('restrct')->on_delete('restrct')は紐付いているこのレコードが存在する場合、親に対する更新/削除はエラーになります。要は関連する子のレコードを全て削除してから、親を操作する場合に指定するものです。勝手に子の情報を変更したり削除したりされてはいけない場合に指定しましょう。

こうしたユニークキーとか外部キーは、どうしても無くてはならないものではありません。プログラム側で完璧な操作が保証できるのでしたら、付ける必要はありません。それなりにオーバーヘッドがかかるので、無くて済むなら、付けないほうが早いです。

しかし、人間が作成するものですから、何らかのミスが混じり込む可能性はいつでも存在し、そのための保証としてはとても安いものです。ですから、特に速度的にシビアだとか、デスク容量を極限まで削るとか、このような制限が無ければきちんと使用するほうが良いでしょう。

次の記事で、修正と追加のコードをお読みいただき、そのあとでまとめて解説していきます。