【CakePHP3】アソシエーションについて解説します




こんにちは、普段CakePHPで開発をしています。開発するにあたってControllerは良く使うのですが、Modelはよくわからない…となんとなく苦手意識があったので、アソシエーションについて勉強してみました。

今回はArticlesテーブルとUsersテーブルをアソシエーションで関連付けして一度にデータを引き出すところまでやってみます。

目次

アソシエーションとは?

アソシエーションは直訳すると「関連」という意味があり、データベース絡みの文脈で言うアソシエーションはテーブル間の関連性を意味しています。具体的には、次の4種類が存在します。

hasOne(1:1)

いま、usersテーブルとprofilesテーブルが存在するとしましょう。プライマリーキーは、どちらもuser_idです。

【users】

user_idname
1Taro
2Hanako

【profiles】

user_idtelborn
1000-000-0000Tokyo
2999-999-9999Osaka

この2つのテーブルの関係を考えてみます。この場合は、どちらもuser_idという同じキーをプライマリーキーにしており、各レコードは1:1で対応しています。この1:1の関係を持つテーブルをhasOneといいます。

hasMany(多:1)

次に、usersテーブルとarticlesテーブルがあるとしましょう。プライマリーキーはそれぞれuser_idとidです。

【users】

user_idname
1Taro
2Hanako

【articles】

iduser_idtitlebody
11はじめての投稿おはようございます
212回目の投稿こんにちは
32はじめましてこんばんは

この場合、2つのテーブルの関係は1:1ではありません。何故ならば、1人のユーザーが複数の記事を書くことがあるからです。usersテーブルのuser_id一つに対して、articlesテーブルにはuser_idに対応するレコードが複数あります。実際、usersテーブルのuser_idが1のTaroには、articlesテーブルで2つのレコードが対応しています。これを「usersテーブルはarticlesテーブルに対してhasManyの関係を持つ」と言います。

belongsTo(1:多)

上で利用したusersテーブルとarticlesテーブルを引き続き使います。プライマリーキーはそれぞれuser_idとidです。

【users】

user_idname
1Taro
2Hanako

【articles】

iduser_idtitlebody
11はじめての投稿おはようございます
212回目の投稿こんにちは
32はじめましてこんばんは

この場合、articlesテーブルのレコードは、user_idをキーとして必ずuserテーブルのどれかのレコードに対応しています。つまりarticlesテーブルのすべてのレコードは、usersテーブルのいずれかのレコードに属している状態になります。このときarticlesテーブルはusersテーブルに対してbelongsToの関係を持つ、と言います。

belongsToMany(多:多)

そして最後に多:多の場合について説明します。例えば、記事は複数のタグを持ち、かつタグは複数の記事に割り振られます。

【articles】

idtitlebodytag
11番目の記事おはようございます1
22番目の記事こんにちは1
33番目の記事こんばんは2
44番目の記事おやすみなさい2

【tags】

idname
1science
2design

 

【中間テーブル】

idarticletag
11science
21design
32science
43science
54design

このように、記事は複数のタグを持っているし、タグは複数の記事に割り当てられる。この関係性をbelongsToManyと言います。

CakePHPのモデルで実装

CakePHPのモデルでアソシエーションを定義するメリットは、関連するテーブルのデータを一度に持ってきてくれることが一つあります。では実際に書いていきましょう。

データベースについて

データベースは以下の2つのテーブルがあるものとします。

【users】

user_idname
1Taro
2Hanako

【articles】

iduser_idtitlebody
11Taroの記事1Taroの記事1です。
22Hanakoの記事1Hanakoの記事1です。
31Taroの記事2Taroの記事2です。
42Hanakoの記事2Hanakoの記事2です。

Model

Modelは上の2つに対応したものを作成します。ちなみに、app名のフォルダのbinにあるcakeの実行ファイルを使って

bin/cake bake model articles
bin/cake bake model users

と実行すると、Modelファイルを自動生成してくれる上に、アソシエーションも自動設定してくれます。では実際に作って見てみましょう。今回不要な部分は削除してあります。

UsersTable.php

<?php
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class UsersTable extends Table
{
    /**
     * Initialize method
     *
     * @param array $config The configuration for the Table.
     * @return void
     */
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('users');
        $this->setDisplayField('name');
        $this->setPrimaryKey('id');

        $this->hasMany('Articles', [
            'foreignKey' => 'user_id'
        ]);
    }
    
    public function validationDefault(Validator $validator)
    {
        $validator
            ->integer('id')
            ->allowEmptyString('id', null, 'create');

        $validator
            ->scalar('name')
            ->maxLength('name', 32)
            ->requirePresence('name', 'create')
            ->notEmptyString('name');

        return $validator;
    }
}

ArticlesTable.php

<?php
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class ArticlesTable extends Table
{
    public function initialize(array $config)
    {
        parent::initialize($config);

        $this->setTable('articles');
        $this->setDisplayField('title');
        $this->setPrimaryKey('id');

        $this->belongsTo('Users', [
            'foreignKey' => 'user_id',
            'joinType' => 'INNER'
        ]);
    }

    /**
     * Default validation rules.
     *
     * @param \Cake\Validation\Validator $validator Validator instance.
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator)
    {
        $validator
            ->integer('id')
            ->allowEmptyString('id', null, 'create');

        $validator
            ->scalar('title')
            ->maxLength('title', 64)
            ->requirePresence('title', 'create')
            ->notEmptyString('title');

        $validator
            ->scalar('body')
            ->maxLength('body', 20000)
            ->requirePresence('body', 'create')
            ->notEmptyString('body');

        return $validator;
    }
    
    public function buildRules(RulesChecker $rules)
    {
        $rules->add($rules->existsIn(['user_id'], 'Users'));

        return $rules;
    }
}

usersテーブルの25行目と、articlesテーブルの19行目を見てください。それぞれhasManyとbelongsToが定義してあります。usersテーブルはarticlesテーブルに対して複数のレコード(記事)が対応するのでhasManyの関係、articlesテーブルのレコードはusersテーブルのどれかに属しているのでbelongsToの関係になっています。

Controller

こちらのControllerも自動生成して、今回不要な箇所を消しました。

bin/cake bake controller articles

ArticlesController.php

<?php
namespace App\Controller;

use App\Controller\AppController;

class ArticlesController extends AppController
{
    public function index()
    {
        $articles = $this->Articles->find('all')->contain('Users')->enableHydration(false)->toArray();
        $this->set(compact('articles'));
        $this->render('index');
    }
}

特に10行目のcontain(‘Users’)を見てください。このcontainは、アソシエーションを設定したUsersテーブルのデータも取ってくるというメソッドです。今回は配列で受け取りたいのでenableHydration(false)->toArray();をつけておきました。

これを$articlesという変数でviewに送ります。

index.ctp

そして、Controllerから送られてきた$articlesをdebug()で表示してみます。

<?php
  foreach ($articles as $article) {
    debug($article);
  }
 ?>

その結果がこちらです。

デバッグ画面

この通り、’user’というキーの中に、記事を書いた著作者の情報が入っています!このようにアソシエーションをうまく活用することで、データのやりとりを効率的に行うことが出来るようになります。

まとめ

たくさんのテーブルを利用するようなウェブアプリケーションでは、テーブル間の関係を把握しておくことはとても大切です。確かにアソシエーションを一つ一つ考えていくのは大変ですが、後々効率的にデータを扱えるようになるので覚えておくのが良いと思いました。

belongsToManyなどは中間テーブルも絡んで頭がこんがらがりそうですが、これから勉強して慣れていこうと思います。