こんにちは、普段CakePHPで開発をしています。開発するにあたってControllerは良く使うのですが、Modelはよくわからない…となんとなく苦手意識があったので、アソシエーションについて勉強してみました。
今回はArticlesテーブルとUsersテーブルをアソシエーションで関連付けして一度にデータを引き出すところまでやってみます。
目次
アソシエーションとは?
アソシエーションは直訳すると「関連」という意味があり、データベース絡みの文脈で言うアソシエーションはテーブル間の関連性を意味しています。具体的には、次の4種類が存在します。
hasOne(1:1)
いま、usersテーブルとprofilesテーブルが存在するとしましょう。プライマリーキーは、どちらもuser_idです。
【users】
| user_id | name |
| 1 | Taro |
| 2 | Hanako |
【profiles】
| user_id | tel | born |
| 1 | 000-000-0000 | Tokyo |
| 2 | 999-999-9999 | Osaka |
この2つのテーブルの関係を考えてみます。この場合は、どちらもuser_idという同じキーをプライマリーキーにしており、各レコードは1:1で対応しています。この1:1の関係を持つテーブルをhasOneといいます。
hasMany(多:1)
次に、usersテーブルとarticlesテーブルがあるとしましょう。プライマリーキーはそれぞれuser_idとidです。
【users】
| user_id | name |
| 1 | Taro |
| 2 | Hanako |
【articles】
| id | user_id | title | body |
| 1 | 1 | はじめての投稿 | おはようございます |
| 2 | 1 | 2回目の投稿 | こんにちは |
| 3 | 2 | はじめまして | こんばんは |
この場合、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_id | name |
| 1 | Taro |
| 2 | Hanako |
【articles】
| id | user_id | title | body |
| 1 | 1 | はじめての投稿 | おはようございます |
| 2 | 1 | 2回目の投稿 | こんにちは |
| 3 | 2 | はじめまして | こんばんは |
この場合、articlesテーブルのレコードは、user_idをキーとして必ずuserテーブルのどれかのレコードに対応しています。つまりarticlesテーブルのすべてのレコードは、usersテーブルのいずれかのレコードに属している状態になります。このときarticlesテーブルはusersテーブルに対してbelongsToの関係を持つ、と言います。
belongsToMany(多:多)
そして最後に多:多の場合について説明します。例えば、記事は複数のタグを持ち、かつタグは複数の記事に割り振られます。
【articles】
| id | title | body | tag |
| 1 | 1番目の記事 | おはようございます | 1 |
| 2 | 2番目の記事 | こんにちは | 1 |
| 3 | 3番目の記事 | こんばんは | 2 |
| 4 | 4番目の記事 | おやすみなさい | 2 |
【tags】
| id | name |
| 1 | science |
| 2 | design |
【中間テーブル】
| id | article | tag |
| 1 | 1 | science |
| 2 | 1 | design |
| 3 | 2 | science |
| 4 | 3 | science |
| 5 | 4 | design |
このように、記事は複数のタグを持っているし、タグは複数の記事に割り当てられる。この関係性をbelongsToManyと言います。
CakePHPのモデルで実装
CakePHPのモデルでアソシエーションを定義するメリットは、関連するテーブルのデータを一度に持ってきてくれることが一つあります。では実際に書いていきましょう。
データベースについて
データベースは以下の2つのテーブルがあるものとします。
【users】
| user_id | name |
| 1 | Taro |
| 2 | Hanako |
【articles】
| id | user_id | title | body |
| 1 | 1 | Taroの記事1 | Taroの記事1です。 |
| 2 | 2 | Hanakoの記事1 | Hanakoの記事1です。 |
| 3 | 1 | Taroの記事2 | Taroの記事2です。 |
| 4 | 2 | Hanakoの記事2 | Hanakoの記事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などは中間テーブルも絡んで頭がこんがらがりそうですが、これから勉強して慣れていこうと思います。










