Lesson 9

アソシエーション

Lesson 9 Chapter 1
モデル同士をつなぐアソシエーション

アソシエーション(関連付け)の概要

アソシエーションとは何かを一言で言うと「データベース内の複数のテーブル(モデル)間における関連性を定義する仕組みのこと」となるのですが、そもそも関連性とはどういうもので、なぜ必要になるのか?といった部分を、これまで作成してきたTODOアプリを例にとって説明していきます。

まず、前回の Lesson8 では、ログイン認証を行うためにusersテーブルを作成し、一意のユーザーとしてログインすることが出来るようになりました。

そして例えば、既存のtasksテーブルとusersテーブルのデータを使って「ユーザーごとに自分が作成したタスクを管理できるようにしたい」と考えたとします。

そのためには、usersテーブルで管理している各ユーザーのデータと、tasksテーブルで管理しているタスクのデータを正しく紐づける必要がでてきます。

9_1_1.png

このように、アプリケーションを作成していく中で、関連性を持たせたいテーブルが出てきた場合に「アソシエーション」が有用となってきます。

ここまでアソシエーションがなぜ必要になってくるのかを見てきましたが、実際に2つのテーブルをレコード単位で関連づけるためには「外部キー」という役割を持つカラムをテーブルに追加する必要があります。

外部キーとして設定されたカラムは、別のテーブルの主キーを参照するために使用され、テーブル間の関連を確立する仕組みとなります。

外部キーについても文字だけではわかりにくいので、以下に usersテーブルとtasksテーブルのレコードを外部キー「user_id」によって結びつけ、2テーブルのデータを一度に取得した場合の図を示します。

9_1_2.png

外部キーとしての users_id カラムがtasksテーブルに追加されており、その値は usersテーブルの主キーである id カラムの値を参照し、同じ値を持つレコードと紐づいています。

また、図の中には「1対多」という表示がありますが、これはテーブル間の関連性がどのように定義されているかを表しています。関連性の定義にはいくつかの種類があり、今回の例では 親である usersテーブルの1レコードが 子である tasksテーブルの複数(1つ以上)のレコードに紐づくことになるため、「1対多(hasMany)」という関係性で定義されています。関連性の種類については、後ほどまとめて説明していきます。

ここまでをまとめると「アソシエーションとはテーブル間に関連性を持たせたいときに有用であり、外部キーの設定と、関連性の定義を行うことで、2テーブルの関連するデータを同時に取得したりすることが出来る」ということになります。

データの取得以外にも、2テーブルの関連するデータをまとめて更新したり、削除したりといったことが出来るようになるため、2テーブル間のデータを必要とする処理を作成する場合などに非常に重宝する仕組みとなります。

アソシエーションの種類

先ほどの usersテーブルとtasksテーブルの例では、「hasMany(1対多)」という関連性の定義でしたが、アソシエーションには hasManyの他に BelongsTo、HasOne、BelongsToMany という定義の種類があり、どの関係性で定義するかは「親テーブル」「子テーブル」「主キー」がどのような関係になっているかによって決まります。

以下、4つの種類について簡単な例とともに見ていきます。

hasOneアソシエーション

hasOneは1対1の関係を表し、1つのレコードが他方のテーブルの1つのレコードと関連づきます。

例えば親テーブルとしてのusersテーブルと、子テーブルとしてのprofiles(プロフィール)テーブルがあり、ユーザー1人につき1つのプロフィールを持っている状態がhasOneとなります。

9_1_3.png

hasManyアソシエーション

hasManyは1対多の関係を表し、1つのレコードが他方のテーブルの複数のレコードと関連づきます。

アソシエーションの概要説明で使用した 親テーブルにusersテーブル、子テーブルにtasksテーブルがあり、ユーザーが複数(1つ以上)の投稿を持っている状態がhasManyの関係となります。

9_1_4.png

belongsTo アソシエーション

belongsToは多対1(or1対1)の関係を表し、複数(もしくは1つ)のレコードが他方のテーブルの1つのレコードと関連づきます。

hasOne(1対1)、hasMany(1対多)が親テーブルから見た子テーブルとの関係を表すのに対して、子テーブルから親テーブルを見た場合の1対1と多対1を表す関係性としての定義と言うことが出来ます。

9_1_5.png

9_1_6.png

belongsToManyアソシエーション

belongsToManyは多対多の関係を表し、他の関係性とは違い、3つ目のテーブルとなる「中間テーブル」というものを用意して表します。

例えば、customers(顧客)テーブルとproducts(商品)テーブルがあり、顧客は複数の品を注文し、商品は複数の顧客から注文されるとした場合などが考えられます。

9_1_7.png

中間テーブルの命名はデフォルトだと2つのテーブルの名前を使用したcustomers_productsテーブルのようになり、外部キーとしてcustomer_idとproduct_idの2つを用意する必要があります。belongsToManyについては少し複雑なため、この後の Chapter3 で改めて解説を行います。

Lesson 9 Chapter 2
アソシエーションの設定とデータの取り出し

関連付けの設定

ここまでアソシエーションの概要とその種類を見てきましたが、ここからは既存のusersテーブルとtasksテーブルに関連付けの設定を行い、ユーザーが持っているタスクを画面上で確認できるようにしていきます。

まず最初に、tasksテーブルに外部キーとなる「user_id」カラムを追加し、各タスクがどのユーザーに属するのかを表せるようにします。

カラムの追加は MySQL のコマンドラインで以下のコマンドを実行します。今回は便宜的にuser_idカラムの値がすべて1となるようにしています。

alter table Tasks add user_id int default 1 after content; 

実行後には、セレクト文でカラムが正常に追加されているか確認をしておきましょう。

select * from tasks;

次に「関連性の定義」を行っていきます。定義を行うにはテーブルクラスにアソシエーションの種類と、関連するテーブル(モデル)の指定を行います。

まずは親から見た子テーブルとの関連性の定義を行うために usersテーブルのクラスを編集します。src/Model/Table/UsersTable.php を開き、クラスのinitializeメソッド内に以下のように記述をします。

UsersTable.php
class UsersTable extends Table
{
    public function initialize(array $config): void
    {
        ...

        //ほかの設定の下にhasManyメソッドを追加
        $this->hasMany('Tasks');
    }
}

呼び出したhasManyメソッドに、子テーブルにあたるtasksテーブルのモデルを指定するため'Tasks'をセットしています。

これで親であるusersテーブルから見たtasksテーブルとの関連性の定義は完了です。

次に、子テーブルであるtasksテーブルから見たusersテーブルとの関連性を定義するために src/Model/Table/TasksTable.php を開き編集を行います。

class TasksTable extends Table
{
    public function initialize(array $config): void
    {
        $this->belongsTo('Users')
             ->setDependent(true);
    }
}

hasMany(1対多)の反対としてbelongsTo(多対1)を定義しています。続くsetDependent()メソッドはtrueを取ることで、親テーブルのレコードが削除された場合に関連を持つ子テーブルのデータも同時に削除するようなります。

今回だとユーザーのデータを削除した場合に、そのユーザーが持っていたタスクのデータも削除されるようになります。

これで2テーブル間の関連性の定義は完了です。

関連付けしたデータの取り出し

関連付けの設定が出来たので、次に2テーブル間で関連付いているレコードのデータを取得してみましょう。

関連づいているテーブルのデータを取得するためには、コントローラでデータの検索を行う際に contain()というメソッドを追加します。

使用方法は簡単で contain()メソッドの引数に、関連付いているテーブル名を渡すのみとなります。

今回はsrc/Contorller/UsersController.phpを開き、以下のようにviewメソッドのcontainに'Tasks'を指定する記述を行いましょう。

UsersController.php
public function view($id = null)
{
    $user = $this->Users->get($id, [
        'contain' => ['Tasks'], //'Tasksを追加
        '
    ]);
    pr($user);//デバッグ関数

    $this->set(compact('user'));
}

データがどのように取得されているのかを確認しておきたいため、デバッグとしてpr()メソッドも追加しています。

ここで一度、ブラウザでユーザ一覧のページを開き、id:1のユーザの 「view」リンクをクリックし詳細ページを開きましょう。pr()メソッドによって変数$userの中身が以下のように表示されます。[tasks]というキーが追加されており、関連づいているレコードのデータが取得されていることがわかります。

App\Model\Entity\User Object
(
    [id] => 1
    [username] => cake
    [password] => $2y$10$yITFmlLMjJemY5U/ElSFhOBBBv606P3KsskeEtQ2V152ip2HeWohy
    [created_on] => 
    [modified_on] => Cake\I18n\FrozenTime Object
        (
            [date] => 2023-05-06 21:35:59.000000
            [timezone_type] => 3
            [timezone] => UTC
        )

    [tasks] => Array
        (
            [0] => App\Model\Entity\Task Object
                (
                    [id] => 1
                    [content] => データベースの接続を完了する。
                    [user_id] => 1
                    [created] => Cake\I18n\FrozenTime Object
                        (
                            [date] => 2023-05-06 21:13:51.000000
                            [timezone_type] => 3
                            [timezone] => UTC
                        )
                          ...

つづいて、テンプレートのsrc/templates/Users/view.phpを編集し、取得したtasksのデータを表示できるようにします。以下ではModifiedの下にtasksを追加しています。

view.php
<tr>
    <th><?= __('Modified') ?></th>
    <td><?= h($user->modified) ?></td>
</tr>
<tr>
    <th><?= __('tasks') ?></th>
    <td>
        <?php foreach($user->tasks as $task): ?>
        <?= h($task->content) .'</br>' ?>
        <?php endforeach; ?>
    </td>
</tr>

記述ができたら、再度ページをリロードしタスクが表示されることを確認しましょう。デバッグのpr()メソッドは削除してしまって構いません。

以上でchapter2は終了です。2つのテーブルの関係をテーブルクラスで指定し、containメソッドを使用することで関連付いたデータを取得することが出来ることを覚えておきましょう。

Lesson 9 Chapter 3
多対多のアソシエーションの例

多対多に関しては他のアソシエーションよりも複雑な関係性となるため、改めて説明と例を上げていきます。

中間テーブルとその必要性

hasOne、hasMany、belongsToでは2つのテーブルのどちらかに外部キーがありました。しかし多対多であるbelongsToManyについては、3つ目のテーブルである「中間テーブル」を用意し、そこに2つの外部キーを置く必要があります。

これはbelongsToMany以外のアソシエーションでは「1対多」「1対1」など、必ず親テーブルに「1」の役割を持つレコードがあり、子テーブル側の関連付けられたレコードは、外部キーを使って「1」の役割を持つレコードだけを参照出来ればよかったためとなります。

9_3_1.png

しかしbelongsToManyでは関連付けようとしている2テーブルの両方において、他方のテーブルの複数のデータと関連を持つため、どちらのテーブルに外部キーを入れても、複数の値を参照することが出来ず、多対多の関係性を表すことが出来ません。

9_3_2.png

そのため多対多の関係性を表すために中間テーブルとなるものを用意し、両テーブルとの外部キーにあたる2つのキーを持たせることで漏れなく関係性を表せるようにします。

9_3_3.png

データの属性を多対多の定義で表す

最後に一つ、多対多のアソシエーションとしてよく見られる例をあげます。

ブログなどで記事を作成したとき、記事にタグ付けをつけるなどして記事の属性を分類しておくケースがよくありますが、タグの種類は「プログラミング」「PHP」「CakePHP」などブログの主旨によって様々な種類が用意され、1つの記事に複数のタグ付けがされることもままあります。

これらを管理するためには、記事を管理する articlesテーブルと、タグを管理するためのtagsテーブルなどを作成する必要があります。

このとき、記事には様々なタグが付けられ、各種タグは様々な記事に使用されるという関係が出来上がります

これはarticlesテーブルから見てtagsテーブルの複数のデータが関連づき、tagsテーブルから見てもarticlesテーブルの複数のデータと関連づく関係性のため belongsToManyが定義として使用されます。

9_3_4.png

また、上記の関係があったとして、テーブルクラスで定義をする場合には以下のようにarticlesとtagsの両方のテーブルクラスでbelongsToManyの定義を入れることになります。

// src/Model/Table/ArticlesTable.php の中で
  class ArticlesTable extends Table
  {
      public function initialize(array $config): void
      {
          $this->belongsToMany('Tags');
      }
  }
  
  // src/Model/Table/TagsTable.php の中で
  class TagsTable extends Table
  {
      public function initialize(array $config): void
      {
          $this->belongsToMany('Articles');
      }
  }

以上で多対多のアソシエーションの例については終了です。アソシエーションは少し複雑に感じるかもしれませんが、ここまでの基本事項をもとに4つの関連性の定義を区別して扱えるようにしておきましょう。