こんにちは、普段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などは中間テーブルも絡んで頭がこんがらがりそうですが、これから勉強して慣れていこうと思います。