Post HABTM Tag, Tag HABTM Post
多対多のデータモデルを構築するために中間テーブルを作成
CakePHPのブログチュートリアルにTagモデルを追加します。
復習
入門データモデリング ブログチュートリアルにCommentモデルを追加のエントリーでは、ブログチュートリアルで作成したPostモデルに紐づいたCommentモデルを作成し、
- hasMany
- belongsTo
双方の使い方について書きました。データモデル入門では、ブログチュートリアルのUserモデルとPostモデルを連携させる方法を実際にhasManyを設定して確認しました。
本記事では、hasAndBelongsToMany (HABTM)を利用して、PostモデルとTagモデルを連携させます。
PostとTagの関係
ブログサービスにおいて、PostとCommentは1対多の関係にありますが、Post(記事)とTag(タグ)の関係は1対多の関係にあるでしょうか?
Post hasMany Tag
の関係は成立しますが、
Tag belongsTo Post
のアソシエーション(多対1の関係)は必ずしも成立しません。厳密にはブログのサービス仕様によって変わってきますが、
A記事に追加されたタグが、B記事に追加された状態
になることもあります。ブログサービスの特定のコメントが複数の記事に紐づくことはありませんが、タグの場合は複数の記事に追加されることがあります。本記事では、そのようなサービス仕様を前提にします。
このような多対多の関係をモデル上で表現するために、CakePHPのhasAndBelongsToMany(HABTM)を使うことが出来ます。この多対多のデータモデルを言葉で表現すると
Post HABTM Tag, Tag HABTM Post
となります。
中間テーブルの作成
hasAndBelongsToMany(HABTM)のアソシエーションを構築するにあたり、postsテーブルとtagsテーブルを中継する中間テーブルを作成します。双方のテーブルと中間テーブルをjoinして関係するデータを取得することになります。
ER図を表現すると
posts → post_tags ← tags
といった関係になります。
postsテーブルはブログチュートリアルで作成してあるので、
- post_tagsテーブル
- tagsテーブル
を新規に作成します。
post_tagsテーブルのSQL
CREATE TABLE IF NOT EXISTS `post_tags` ( `id` int(10) unsigned NOT NULL, `post_id` int(10) unsigned NOT NULL DEFAULT '0', `tag_id` int(10) unsigned NOT NULL DEFAULT '0', `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `post_tags` (`id`, `post_id`, `tag_id`, `created`, `modified`) VALUES (1, 1, 1, '2014-11-01 09:05:12', NULL), (2, 1, 2, '2014-11-01 09:05:12', NULL), (3, 2, 1, '2014-11-01 20:38:55', NULL), (4, 4, 2, '2014-11-01 20:38:55', NULL);
tagsテーブルのSQL
CREATE TABLE IF NOT EXISTS `tags` ( `id` int(10) unsigned NOT NULL, `tag` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '', `tagSlug` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL, `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `tags` (`id`, `tag`, `tagSlug`, `created`, `modified`) VALUES (1, 'tag1', 'tag1', '2014-11-01 09:05:12', NULL), (2, 'tag2', 'tag2', '2014-11-01 09:05:12', NULL), (3, 'tag3', 'tag3', '2014-11-02 02:02:32', NULL), (4, 'tag4', 'tag4', '2014-11-02 02:02:32', NULL);
データベース側の物理構造を定義することができました。
CakePHPのモデルでhasAndBelongsToManyを設定
PostモデルとTagモデルにhasAndBelongsToManyを正しく設定することにより、関係するデータモデルを取得できるようになります。
Postモデル側
public $hasAndBelongsToMany = array( 'Tag' => array( 'className' => 'tag', 'joinTable' => 'post_tags', 'foreignKey' => 'post_id', 'associationForeignKey' => 'tag_id', 'unique' => true, 'conditions' => '', 'fields' => '', 'order' => '', 'limit' => '', 'offset' => '', 'finderQuery' => '', 'deleteQuery' => '', 'insertQuery' => '' ) );
Tagモデル側
public $hasAndBelongsToMany = array( 'Post' => array( 'className' => 'post', 'joinTable' => 'post_tags', 'foreignKey' => 'tag_id', 'associationForeignKey' => 'post_id', 'unique' => true, 'conditions' => '', 'fields' => '', 'order' => '', 'limit' => '', 'offset' => '', 'finderQuery' => '', 'deleteQuery' => '', 'insertQuery' => '' ) );
データベーステーブルとCakePHPのモデルが適切に設定されていれば、CakePHPのfindメソッドを使って多対多のデータ構造を取得できるようになります。
findの実行
コントローラ側からfindを実行して返されるデータ構造を確認してみます。
Postモデル側からfind
debug($this->Post->findById(1));
以下のデータ構造を取得することが可能です。Post(記事)に紐づくTag(タグ)を取得できています。
array( 'Post' => array( 'id' => '1', 'title' => 'タイトル', 'body' => 'これは、記事の本文です。', 'created' => '2014-12-01 20:11:58', 'modified' => null, 'user_id' => '1' ), 'Tag' => array( (int) 0 => array( 'id' => '1', 'tag' => 'tag1', 'tagSlug' => 'tag1', 'created' => '2014-11-01 09:05:12', 'modified' => null, 'PostTag' => array( 'id' => '1', 'post_id' => '1', 'tag_id' => '1', 'created' => '2014-11-01 09:05:12', 'modified' => null ) ), (int) 1 => array( 'id' => '2', 'tag' => 'tag2', 'tagSlug' => 'tag2', 'created' => '2014-11-01 09:05:12', 'modified' => null, 'PostTag' => array( 'id' => '2', 'post_id' => '1', 'tag_id' => '2', 'created' => '2014-11-01 09:05:12', 'modified' => null ) ) ) )
Tagモデル側からfind
debug($this->Tag->findById(1));
特定のタグとそれに紐づく記事を取得出来ていることが確認できます。
array( 'Tag' => array( 'id' => '1', 'tag' => 'tag1', 'tagSlug' => 'tag1', 'created' => '2014-11-01 09:05:12', 'modified' => null ), 'Post' => array( (int) 0 => array( 'id' => '1', 'title' => 'タイトル', 'body' => 'これは、記事の本文です。', 'created' => '2014-12-01 20:11:58', 'modified' => null, 'user_id' => '1', 'PostTag' => array( 'id' => '1', 'post_id' => '1', 'tag_id' => '1', 'created' => '2014-11-01 09:05:12', 'modified' => null ) ), (int) 1 => array( 'id' => '2', 'title' => 'またタイトル', 'body' => 'そこに本文が続きます。', 'created' => '2014-12-01 20:11:58', 'modified' => null, 'user_id' => '2', 'PostTag' => array( 'id' => '3', 'post_id' => '2', 'tag_id' => '1', 'created' => '2014-11-01 20:38:55', 'modified' => null ) ) ) )
CakePHPを利用して多対多のデータモデルを構築することが出来ました。
アソシエーション:hasAndBelongsToMany(HABTM) CakePHP Cookbook 2.xドキュメント