CakePHP4の規約外のカラムをキーにアソシエーション(テーブル連結)する方法
2025/01/31
CakePHPの規約外のカラムをキーにアソシエーション(テーブル連結)する方法
CakePHP4、CakePHP5で CakePHPの規定から外れることでデフォルトではアソシエーション(テーブルの関連付け)されないカラム(項目)をキーとしてアソシエーションする方法を解説します。
※この記事の内容は、CakePHP4系でも、CakePHP5系でも動作することを確認しています。
前提:存在するテーブルのイメージ
解説する環境には下記の 3つのテーブルがあるとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
CREATE TABLE `companies` ( `id` int(11) NOT NULL UNIQUE COMMENT '企業ID', `company_name` varchar(50) DEFAULT NULL COMMENT '企業名', `company_tel` varchar(20) DEFAULT NULL COMMENT '電話番号', `created_user_id` int(11) DEFAULT NULL COMMENT '登録ユーザID', `modified_user_id` int(11) DEFAULT NULL COMMENT '更新ユーザID', `created` datetime NOT NULL DEFAULT now() COMMENT '登録日時', `modified` datetime NOT NULL DEFAULT now() COMMENT '更新日時', PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`) ) ENGINE=InnoDB COMMENT='企業情報'; CREATE TABLE `company_details` ( `id` int(11) NOT NULL UNIQUE COMMENT '企業詳細ID', `company_id` int(11) NOT NULL COMMENT '企業ID', `trading_history` text DEFAULT NULL COMMENT '取引履歴', `created_user_id` int(11) DEFAULT NULL COMMENT '登録ユーザID', `modified_user_id` int(11) DEFAULT NULL COMMENT '更新ユーザID', `created` datetime NOT NULL DEFAULT now() COMMENT '登録日時', `modified` datetime NOT NULL DEFAULT now() COMMENT '更新日時', PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`) ) ENGINE=InnoDB COMMENT='企業情報詳細'; CREATE TABLE `users` ( `id` int(11) NOT NULL UNIQUE COMMENT 'ユーザID', `user_name` varchar(50) DEFAULT NULL COMMENT 'ユーザ名', `user_tel` varchar(20) DEFAULT NULL COMMENT 'ユーザ電話番号', `created_user_id` int(11) DEFAULT NULL COMMENT '登録ユーザID', `modified_user_id` int(11) DEFAULT NULL COMMENT '更新ユーザID', `created` datetime NOT NULL DEFAULT now() COMMENT '登録日時', `modified` datetime NOT NULL DEFAULT now() COMMENT '更新日時', PRIMARY KEY (`id`), UNIQUE KEY `id_UNIQUE` (`id`) ) ENGINE=InnoDB COMMENT='ユーザ情報'; |
Bakeで自動生成されるアソシエーション
これらのテーブルに対して、CakePHPの Bakeを使用して「company_details」の Modelを生成してみます。
「company_details.company_id」は、CakePHPの規約に従ってカラム名が指定されていますので、デフォルトでカラム「company_details.company_id」をキーとしてテーブル「companies」とアソシエーション(連結)する処理が生成されます。
具体的には、「/src/Model/Table/CompanyDetailsTable.php」に下記の記述が生成されますが、11~14行目がそれにあたります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public function initialize(array $config): void { parent::initialize($config); $this->setTable('company_details'); $this->setDisplayField('id'); $this->setPrimaryKey('id'); $this->addBehavior('Timestamp'); $this->belongsTo(Companies', [ 'foreignKey' => 'company_id', 'joinType' => 'INNER', ]); } |
「created_user_id」「modified_user_id」をキーに「Users」とアソシエーションする
「created_user_id」「modified_user_id」は、各テーブルのレコードを登録、更新した際に、それを実行したユーザの IDを保存するカラムで、カラム「users.id」の情報が入っています。
そのため、「created_user_id」「modified_user_id」をテーブル「Users」とアソシエーションしたいと思いますが、CakePHPの規定に則っていないカラム名ですので、自動的にはアソシエーションの処理はしてくれません。
では、「created_user_id」「modified_user_id」をテーブル「Users」とアソシエーション(関連付け)するにはどうすればいいでしょうか?
と言う対応をしていきます。
「created_user_id」「modified_user_id」はすべてのテーブルにありますので、どのテーブルでもいいのですが、「company_details」への対応をサンプルとします。
Modelにアソシエーションの情報を記述
まず初めに、「/src/Model/Table/CompanyDetailsTable.php」に下記の記述を追記します。
先に紹介した 14行目に続けて追記するといいでしょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 「登録ユーザ」「更新ユーザ」の情報のアソシエーション $this->hasOne('CreatedUser', [ "className" => "Users", 'foreignKey' => 'id', 'bindingKey' => 'created_user_id', 'joinType' => 'LEFT', ]); $this->hasOne('ModifiedUser', [ "className" => "Users", 'foreignKey' => 'id', 'bindingKey' => 'modified_user_id', 'joinType' => 'LEFT', ]); |
アソシエーション情報の各項目の解説
「$this->hasOne('ModifiedUser', [
」の「ModifiedUser」はアソシエーションの名称です。
この名称を使用して Controllerで containします。
記述はアッパーキャメルケース(最初の文字も大文字のキャメルケース)です。
また、後述しますがここの名称は単数形の単語をした方がよさそうです。
「className」は、アソシエーションする先の Model名です。
「foreignKey」は、アソシエーションする先のカラム名です。「ID」で連結することが多いと思いますが、「foreignKey」を指定する事で「ID」以外も連結できます。
(未検証ですが、複数のキーを指定する方法もあるようです。)
「bindingKey」は、アソシエーション元のカラム名です。
「joinType」は、アソシエーションのタイプで「LEFT」と「INNER」があります。デフォルトは「LEFT」。
アソシエーションの名称を付ける際の注意点
上記のサンプルでは、アソシエーションの名称を「ModifiedUser」と付けています。
ですが、「bindingKey」が「modified_user_id」ですので、「ModifiedUserId」の方がいいかな、などと思いながらそれを設定すると、下記のようなエラーになります。
Warning (512) : Association property name "created_user_id" clashes with field of same name of table "company_details". You should explicitly specify the "propertyName" option. [in C:\xampp\htdocs\vendor\cakephp\cakephp\src\ORM\Association.php, line 111]
当たり前と言えば当たり前なのですが、アソシエーションの名称は、テーブル内にカラム名を追加するようなイメージですので、すでにある「modified_user_id」と同じ「ModifiedUserId」を付けると、ダブってしまいますので、エラーとなるのです。
(スネークケースとキャメルケース(パスカルケース)の違いはあっても同じと認識されるようです。)
そのため、アソシエーションの名称は、既存のカラム名とは違う名称を付ける必要があります。
長くなりますが「Association」+「カラム名」で「AssociationModifiedUserId」にする、みたいなルールを決めておくといいでしょう。
「joinType」の「LEFT」と「INNER」について
「joinType」は、連結するレコードがない場合にどう処理するのか、を指定する区分です。
「LEFT」は、連結先のレコードがない場合は「null」の値を持つレコードがあるものとして処理をします。
「INNER」は、連結先のレコードがない場合は連結元のレコードもないものとして処理します。
今回のように「company_details」から「companies」の情報を見に行く場合は、「companies」がない状況は存在しないので、「INNER」でも問題は起こらないでしょう。
ですが、「companies」から「company_details」の情報を見る場合、「company_details」のレコードは存在しない場合もあるかもしれません。
この時「joinType」に「INNER」を指定していると、「companies」のレコードもないものとして取得することができません。
Controllerにアソシエーションを読み込む情報を記述
次に、Controllerの対応です。
今回はサンプルとして「view」アクションに追加する処理です。
「/src/Controller/CompanyDetailsController.php」ファイルの「view」アクションに下記の処理を追記します。
Bakeするとデフォルトでは「contain句」には「Companies」が記述されていますが、これに、「Model」に追記したアソシエーション名を追記します。
1 2 3 4 5 6 7 8 |
$contractSeo = $this->CompanyDetails->get($id, [ 'contain' => [ 'Companies', "CreatedUser", // 登録ユーザID "ModifiedUser", // 更新ユーザID ], ]); |
Viewテンプレートに値を取得する処理を記述
最後に viewテンプレートでの対応です。
「/templates/CompanyDetails/view.php」に記述します。
viewテンプレートでは「$companyDetail->created_user
」で取得することができます。
「created_user」は、Modelに記述したアソシエーション名をスネークケース(アンダースコアで単語をつなぐ記述方法)で記述します。
「$companyDetail->created_user
」には、「Users」の情報がオブジェクトとして取得していますので、必要に応じて値を取得します。
1 2 3 4 5 6 7 8 9 10 11 |
// 下記の記述で「Users」のオブジェクト全体を確認できます。 var_export($companyDetail->created_user); // 「Users.id」を表示します echo $companyDetail->created_user->id; // 「Users.user_name」を表示します echo $companyDetail->created_user->user_name; // 「created_user_id」があれば Usersの Viewにリンクを設定する、と言う処理 echo $companyDetail->has('created_user_id') ? $this->Html->link($companyDetail->created_user->id . " " . $companyDetail->created_user->name, ['controller' => 'Users', 'action' => 'view', $companyDetail->created_user->id]) : ''; |
Viewテンプレートで呼び出す際の注意点
「created_user_id」では問題は発生しませんが、カラム名によっては Viewテンプレートで値を取得した際にエラーが発生する場合があります。
具体的には「user_sales_id」の「sales」のように最後が「s」で終わる単語を使う場合は気を付けましょう。
例えば、先のテーブル「company_details」に「user_sales_id(担当営業)」と言う項目があり、これにアソシエーションの設定をするとします。
この場合、モデルの「/src/Model/Table/CompanyDetailsTable.php」には下記のように記述するとします。
カラム名が「user_sales_id」ですのでアソシエーション名は「UserSales」とします。
1 2 3 4 5 6 |
$this->hasOne('UserSales', [ "className" => "Users", 'foreignKey' => 'id', 'bindingKey' => 'user_sales_id', 'joinType' => 'LEFT', ]); |
「/src/Controller/CompanyDetailsController.php」ファイルの「view」アクションの「contain句」には「UserSales」を追記します。
ここまでは、先に紹介した方法と何も違いがありません。
1 2 3 4 5 6 7 8 9 10 |
$contractSeo = $this->CompanyDetails->get($id, [ 'contain' => [ 'Companies', "UserSales", // 営業担当ID "CreatedUser", // 登録ユーザID "ModifiedUser", // 更新ユーザID ], ]); |
そして、Viewテンプレート「/templates/CompanyDetails/view.php」では「user_sale」で取得します!!
1 2 3 4 5 6 7 8 |
// 正しく値を取得できる var_export($companyDetail->user_sale); // null になる var_export($companyDetail->user_sales); // 「『id』はないよ」と言うエラーになる var_export($companyDetail->user_sales->id); |
ここが非常に重要なポイントです。
モデルで指定したアソシエーション名は「UserSales」ですので、Viewテンプレートで取得する際は「user_sales」だと思ってしまいますが、なんと「user_sale」なのです!!
「営業」と言う意味で「sales」を使っているので「UserSales」の「Sales」を複数形だという認識は全くないわけなんですが、CakePHPは複数形だと認識して勝手に単数形にしてくれちゃうんです!
と言うわけで、アソシエーション名は単数形で指定する方が無難です。
ただ、「Sales」と「Sale」では意味が違うので、「UserSales」のように最後が「s」で終わる単語を使用する場合は Viewテンプレートで値を取得する際に注意しましょう。
デフォルトとは異なる処理をしているので、Viewテンプレートでエラーが発生しても Modelや Controllerの方の記述のミスじゃないかと思ってそちらばかり見てしまいますが、実は、Viewテンプレートの記述の方だった、ということでなかなか気づけない不具合かと思います。
最後に。
あわせて、オフィシャルサイトの Cakebookも参照してください。
https://book.cakephp.org/4/ja/orm/associations.html
CakePHP4の関連記事
CakePHP4系でJSONレスポンスの処理ではwithStringBodyを使う。3との違い解説CakePHP4、CakePHP5の「warning: DebugKit is disabling...」の対処方法
MySQL+CakePHPのdate型、datetime型項目は「2999-12-31」までしか扱えない
CakePHP4のFrozenDateで1ヵ月前、先月、今月1日、来月末の日付などを算出する方法
CakePHP4のcake cache clear_allでPermission deniedはパーミッションの変更が必要
CakePHP4のクリエビルダーを使用してOR条件をAND条件でつなぐSQL文を作る方法
CakePHP4のController内でViewテンプレート、レイアウトの変更設定を記述する方法
CakePHP4から外部のデータベースにアクセスする方法解説
CakePHP4の数値項目は「like %10%」の部分一致検索(find select)はできない
CakePHP4でロギングスコープやログレベルを使用してログを出し分ける方法を解説
その他の「CakePHP4」に関する記事一覧
GoogleAdwords
GoogleAdwords
この記事が参考になったと思いましたらソーシャルメディアで共有していただけると嬉しいです!
関連記事
-
-
MySQL、CakePHP 2.3で「tinyint(1)」の Boolean型の動作を再確認
MySQL+CakePHPの環境で「tinyint(1)」を利用する際の動作を検証。「tinyint(1)」の Boolean型について CakePHPでは自動処理が実施されていることを確認しました。
-
-
CakePHP4、5で$_SERVERと同じようにURIを取得する「getUri()」の紹介
PHPでサーバ情報、環境情報を取得する際は「
$_SERVER
」を利用するが、似たような情報をCakePHPの関数で取得する際は「getUri()
」を使用する。取得出来る値は一致するものもあるが、違うものもあり便利な使い方もある。
-
-
CakePHP3でPHPExcelを使ってエクセルファイルを生成、出力する方法
CakePHP3でPHPExcelを利用してエクセルを編集、出力するサンプルソース+解説。PHPExcelのインストール方法の解説からファイル保存とダウンロードの方法なども解説。
-
-
CakePHP 2.3 テーブルの項目を演算した結果を条件として抽出する方法
アソシエーション(連携)している先のテーブルの項目で演算をする場合の考え方と注意点をサンプルソースを用いて説明しています。分かってしまえば簡単です。
-
-
CakePHP 2.3でファイルのアップロード処理を作る
CakePHPでプラグインを使わないファイルアップロード処理を解説します。簡単です。DBにファイルを格納する方法も。
-
-
CakePHP 2.3 Search Pluginで検索処理 その7queryを使って 日付の範囲検索
CakePHPの検索プラグイン Search Pluginの検索処理の中で queryを使って日付の範囲検索の方法です。
-
-
CakePHP3、CakePHP4、CakePHP5のバージョンを指定してインストールする詳細な手順を解説
CakePHP3のバージョンを指定してインストールする方法を詳細解説。CakePHP3のインストールはComposerを使うため設定もほぼ自動で完了。データベースの接続情報を記載すればアプリ開発のベースが整う。
-
-
CakePHP3にOGPをfetch、asignを利用してテンプレートごとに指定する方法を解説
CakePHP3でOGPを設定する方法を解説。fetch、assignを使用しレイアウトファイルに編集した変数にテンプレートファイルから値を指定する。これを利用してOGPを編集する。
-
-
CakePHP3にデイトピッカー jQuery UI DatePickerを実装する手順の解説
CakePHP3にjQuery UIのDatePickerを実装する手順を説明。併せて、デイトピッカーを設置に関連するCakePHP3の解説と、テーマを変更したり、表記を変更するカスタマイズする方法なども紹介。
-
-
CakePHP3のCakeDC/Usersの画面、メール本文テンプレートのカスタマイズ方法解説
CakeDC謹製Usersプラグインの紹介。ユーザ新規登録の流れを紹介しつつテンプレートファイルがどこにあるか、設定情報ファイルがどこにあるか、を説明しつつカスタマイズの方法を解説します。