hasAndBelongsToManyを活用して多対多のデータモデルを構築する方法

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ドキュメント

Webエンジニアブログにコメント

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

hasAndBelongsToManyを活用して多対多のデータモデルを構築する方法の記事にコメントを投稿