CakePHP3の1対多での連携を中間テーブルを使った多対多の連携に変更するときの手順
2020/04/18
CakePHP3で「1対多」から「多対多」の連携に変更する手順
「1対多」と中間テーブルを使った「多対多」連携について
CakePHP3で「1対多」のテーブル連携を、中間テーブルを利用した「多対多」のテーブル連携方式に変えるときの手順を紹介します。
どのような場合に必要になるか、というと
例えば、
Topics、Categoriesというテーブルがあるとします。
Topicsは Categoryによって分類されています。
当初は、Topicsの記事はそれぞれに 1つの Categoryが割り当てられていました。
ですが、運用を始めてみたら、Topicsの記事は複数の Categoryにまたがるものもあることが判明しました。
そのため、それまでは「1対多」だったテーブル構成を、中間テーブル作成して「多対多」に対応できるようにすることにしました。
その時の対応方法の手順の紹介です。
サンプルの「categories」と「topics」のテーブルは以下の想定です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
CREATE TABLE `categories` ( `id` smallint(6) NOT NULL AUTO_INCREMENT, `parent_id` smallint(6) DEFAULT NULL, `lft` smallint(6) NOT NULL, `rght` smallint(6) NOT NULL, `name` varchar(100) NOT NULL, `created` datetime NOT NULL, `modified` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 CREATE TABLE `topics` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `category_id` smallint(6) NOT NULL, `topics_date` date NOT NULL, `title` text NOT NULL, `body` text NOT NULL, `created` datetime NOT NULL, `modified` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
カテゴリは、下記の記事でも紹介していますが、CakePHP3のブログチュートリアルにあるツリービヘイビアの機能を利用したカテゴリの構造になっています。
CakePHP3のfriendsofcake/searchでツリーカテゴリーの子階層も含めて検索する方法
「1対多」から中間テーブルを使った「多対多」連携に変更する手順
先に、「例えば」と書いてしまいましたが、まさに、今回私が必要になった状況です。
最初は Topicsに対して 1つの Categoryで足りると思っていましたが、topicsの内容によっては複数の Categoryに属する場合があり、対応を迫られたのです。
CakePHP3で中間テーブルを利用した処理は下記の検索処理などで使っていました。
CakePHP3のfriendsofcake/searchでブックマークチュートリアルのタグ検索を実装
そのため、構築の仕方は理解していましたが、今回の対応を行うときに対応漏れがあり、うまく動かずに悩んだことがありました。
そのため、今後のためにも手順を記録しておくことにしました。
中間テーブルを作成する
まず最初に中間テーブルを作成します。
中間テーブルは、「topic_id」と「category_id」を持つテーブルです。
|
1 2 3 4 5 6 7 8 |
CREATE TABLE `topics_categories` ( `topic_id` int(11) unsigned NOT NULL, `category_id` smallint(6) NOT NULL, PRIMARY KEY (`topic_id`,`category_id`), KEY `category_key` (`category_id`), CONSTRAINT `topic_category_key` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`), CONSTRAINT `topic_key` FOREIGN KEY (`topic_id`) REFERENCES `topics` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
「category_id」カラムを削除
Topicsテーブルから不要になる「category_id」カラムを削除します。
(削除しなくても問題は発生しませんが、不要なカラムは削除する方が望ましいでしょう。)
|
1 |
ALTER TABLE `topics` DROP `category_id`; |
TopicTable.phpの更新
続いて、Tableファイルの更新になりますが、「TopicTable.php」と「CategoriesTable.php」とそれぞれ変更する箇所があります。
まず初めに「TopicTable.php」は、下記の通り、「Categories」テーブルとの「belongsTo」を止めて「belongsToMany」に変更します。
/src/Model/Table/TopicTable.php
|
1 2 3 4 5 6 7 8 9 10 11 |
// 追加 $this->belongsToMany('Categories', [ 'foreignKey' => 'topic_id', 'targetForeignKey' => 'category_id', 'joinTable' => 'topics_categories' ]); // 削除(古い設定を外す) // $this->belongsTo('Categories', [ // 'foreignKey' => 'category_id' // ]); |
CategoriesTable.phpの更新
「CategoriesTable.php」は、下記の通り、「Topics」テーブルとの「belongsToMany」を追加します。
今回は設定がありませんでしたが「hasMany」の設定をしている場合は、その処理は削除します。
/src/Model/Table/CategoriesTable.php
|
1 2 3 4 5 6 |
// 追加 $this->belongsToMany('Topics', [ 'foreignKey' => 'category_id', 'targetForeignKey' => 'topic_id', 'joinTable' => 'topics_categories' ]); |
inputTopics.phpの更新
カテゴリの選択する箇所のテンプレートを更新します。
ここでは、マルチセレクト(複数選択式)のセレクトボックスを設定しています。
/src/Template/Element/inputTopics.php
|
1 2 3 |
// 変更 // echo $this->Form->control('category_id', ['options' => $categories, 'empty' => true]); echo $this->Form->control('categories._ids', ["type"=>"select","multiple"=>"select",'options'=>$categories]); |
Entity/Topic.phpの更新
ここ以降の項目が対応忘れが発生しやすい箇所になります。
まず 1つ目が「Entity」です。
下記のように「category」は「categories」のように複数形になります。
コメントにも書いていますが、これを記述していないと値が保存されません。エラーは出ませんので原因を特定しにくいエラーです。
「'*' => true,」で設定している場合は特に対応は必要ありません。
/src/Model/Entity/Topic.php
|
1 2 3 |
// 変更(※これがないと保存されない) // 'category' => true 'categories' => true |
TopicsController.phpの変更
コントローラーの変更ポイントです。
get()や find()で値を取得する際に、関連テーブルとして「Categories」を指定する必要があります。
コメントに書いていますが、これがないと、テーブルには保存されたレコードがあるのにも関わらず、その情報が更新画面に出てこない、というエラーになります。これもエラーが出ないため、原因を特定しにくいエラーです。
/src/Contorller/TopicsController.php
|
1 2 3 4 5 |
// 追加 ※contain に Categories を追加 // ※これがないと保存された情報が更新画面に反映されない $topic = $this->Topics->get($id, [ 'contain' => ["Categories"] ]); |
中間テーブルを使った「多対多」の項目「category_id」を検索条件にする場合の TopicsController.phpの変更
単純に指定したレコードだけを get()で取得する場合は問題ありませんが、「category_id」を検索条件に検索する場合は記述する内容が大きく変わります。
/src/Contorller/TopicsController.phhp
|
1 2 3 4 5 6 7 8 |
// 変更 ※Category_id を条件にしていた箇所は下記のように修正 // ※単純な where句ではなくなる // $condition = ["category_id"=>$categoryId]; // $query = $this->Topics->find("Open")->contain(["Categories"])->where($condition); $query = $this->Topics->find() ->matching("Categories",function($q) use($categoryId){ return $q->where(["category_id"=>$categoryId]); }); |
細かなところですが、条件の「"category_id"=>$categoryId]」の「$categoryId」を変数とするなら「use($categoryId)」の記述が必要です。
「$categoryId」を変数ではなく値を直接記述する場合は use句は記述する必要はありません。
CakePHP3の ORM matchingメソッドについて詳しく知りたい場合は、下記の記事などを参考にしてみてはいかがでしょうか?
https://s8a.jp/cakephp-3-matching
https://book.cakephp.org/3.0/ja/orm/query-builder.html#id17
view.ctpの変更
最後に、データの取り出しの際も変更の注意点があります。
「category」は複数入ることがあるためここも複数形の「categories」にする必要があります。
/src/Template/Topics/view.ctp
|
1 2 3 |
// 変更 // $topics = $topics->category; $topics = $topics->categories; |
CakePHP3で中間テーブルを扱う場合の関連記事
中間テーブルはちょっとだけ仕組みの理解が必要なため、中間テーブルについてもう少し理解を深めたい、という場合もあるでしょう。
そういう場合は下記の記事なども参考になるかと思います。
CakePHP3のfriendsofcake/searchでブックマークチュートリアルのタグ検索を実装
CakePHP3 ブログチュートリアル
https://book.cakephp.org/3/ja/tutorials-and-examples/blog/blog.html
CakePHP3の関連記事
CakePHPのpostlinkで生成した削除リンクをクリックしても処理が実行されない対処法CakePHP4のCSS、JavaScript、画像のブラウザへのキャッシュをコントロールする
CakePHP3でレコードを保存(追加、更新、Insert、Update)する複数の方法を紹介
CakePHP3でモデルなしフォームからCSVをアップロードしレコードを更新する方法解説
CakePHP3でPHP Simple HTML DOM Parserを使ってスクレイピングする方法
CakePHP3のInsert On Duplicate Key Update(upsert)構文を解説・バルク処理も
CakePHP3の1対多での連携を中間テーブルを使った多対多の連携に変更するときの手順
CakePHP3でデフォルトのソート条件を設定してユーザの選択肢たソート条件を有効にする方法
CakePHP3で Ajaxを使う方法の解説。3.6以降対応。Successとthenの両方を解説。
CakePHP3でパンくずの指定は HTMLヘルパーを使って指定する方法を解説
その他の「CakePHP3」に関する記事一覧
GoogleAdwords
GoogleAdwords
この記事が参考になったと思いましたらソーシャルメディアで共有していただけると嬉しいです!
関連記事
-
-
CakePHP4で「app_local.php」「.env」を利用して環境ごとの定数を振り分ける方法
CakePHP4で.env、app_local.phpに定数を定義してそれを呼び出す方法の解説。Gitでは管理せず本番環境と開発環境とで異なる定数を定義するためそれを利用する方法。
-
-
CakePHP3にOGPをfetch、asignを利用してテンプレートごとに指定する方法を解説
CakePHP3でOGPを設定する方法を解説。fetch、assignを使用しレイアウトファイルに編集した変数にテンプレートファイルから値を指定する。これを利用してOGPを編集する。
-
-
CakePHP4で定数の設定と呼び出し方法の解説(defineとConfigure)
CakePHP4で定数を設定、使用する方法を解説。定数定義はdefineとConfigureを使用する方法を解説。また、bootstrap.phpに直接記述する方法と別のファイルにする方法を解説。
-
-
CakePHP4のController内でViewテンプレート、レイアウトの変更設定を記述する方法
CakePHP4でテンプレートやレイアウトファイルをデフォルトから変更する場合は「render()」を使用するが、記述場所はできるだけコントローラー内の最後の方に書く方がいい。
-
-
CakePHP3でシェルを作成しコマンドラインから実行・CakePHP2との違い
CakePHP3のシェルスクリプトを作成し、コマンドラインから実行する方法を解説。複数単語をつなげる場合の対応方法がCakePHP2より制限が厳しくなったのでCakePHP3の命名規則の確認が必要だ。
-
-
CakePHP4のCakeDC/Usersの画面、メール本文テンプレートのカスタマイズ方法解説
CakeDC謹製Usersプラグインの紹介。ユーザ新規登録の流れを紹介しつつ、テンプレートファイル、設定情報ファイルの場所とそれらをカスタマイズする方法を説明します。
-
-
CakePHP3でユーザ定義の定数、変数を設定し、読み込む方法解説
CakePHP3で定数や共通で使う変数をまとめて設定し、プログラム内で読み込む方法を、bootstrap.phpに直接記述する方法と定数ファイルを分ける方法の3つの方法で解説。
-
-
CakePHP4、5で$_SERVERと同じようにURIを取得する「getUri()」の紹介
PHPでサーバ情報、環境情報を取得する際は「
$_SERVER」を利用するが、似たような情報をCakePHPの関数で取得する際は「getUri()」を使用する。取得出来る値は一致するものもあるが、違うものもあり便利な使い方もある。
-
-
CakePHP3のInsert On Duplicate Key Update(upsert)構文を解説・バルク処理も
CakePHP3で Insert … On Duplicate Key Update構文(upsert)を実行する方法を解説。バルク処理の方法も用意されているため大量処理の場合も対応可能。
-
-
CakePHP4、5の認証処理で認証が通らない際の確認方法と確認箇所の紹介
CakePHP4、5系の認証処理でログイン認証が通らない場合の確認方法、確認箇所を解説。ログ出力し、ステータスを確認するが、ステータスの内容も紹介。それはそのままusernameを変更する際のポイントでもある。
