CakePHP4の規約外のカラムをキーにアソシエーション(テーブル連結)する方法
2025/01/31
CakePHPの規約外のカラムをキーにアソシエーション(テーブル連結)する方法
CakePHP4、CakePHP5で CakePHPの規定から外れることでデフォルトではアソシエーション(テーブルの関連付け)されないカラム(項目)をキーとしてアソシエーションする方法を解説します。
※この記事の内容は、CakePHP4系でも、CakePHP5系でも動作することを確認しています。
前提:存在するテーブルのイメージ
解説する環境には下記の 3つのテーブルがあるとします。
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行目がそれにあたります。
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行目に続けて追記するといいでしょう。
// 「登録ユーザ」「更新ユーザ」の情報のアソシエーション
$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」に追記したアソシエーション名を追記します。
$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」の情報がオブジェクトとして取得していますので、必要に応じて値を取得します。
// 下記の記述で「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」とします。
$this->hasOne('UserSales', [
"className" => "Users",
'foreignKey' => 'id',
'bindingKey' => 'user_sales_id',
'joinType' => 'LEFT',
]);
「/src/Controller/CompanyDetailsController.php」ファイルの「view」アクションの「contain句」には「UserSales」を追記します。
ここまでは、先に紹介した方法と何も違いがありません。
$contractSeo = $this->CompanyDetails->get($id, [
'contain' => [
'Companies',
"UserSales", // 営業担当ID
"CreatedUser", // 登録ユーザID
"ModifiedUser", // 更新ユーザID
],
]);
そして、Viewテンプレート「/templates/CompanyDetails/view.php」では「user_sale」で取得します!!
// 正しく値を取得できる 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の関連記事
CakePHPのpostlinkで生成した削除リンクをクリックしても処理が実行されない対処法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」に関する記事一覧
この記事が参考になったと思いましたらソーシャルメディアで共有していただけると嬉しいです!
関連記事
-
-
CakePHP3でレコードを保存(追加、更新、Insert、Update)する複数の方法を紹介
CakePHP3でレコードを追加、更新(Insert、Update)する記述方法を解説。1件ずつ処理、全件をまとめて処理、条件に該当する複数件のレコードを処理方法をサンプルコードを用いて解説。
-
-
CakePHP5系でDeprecatedを回避しfindListでキーと値のカラムを指定して取得する方法
CakePHP4のfindListでキーと値のカラムを指定してテーブルにアクセスする方法がCakePHP5では「Deprecated(非推奨)」となった。推奨の記述方法を解説。
-
-
CakePHP 2.3 ログイン、操作履歴、アクセスログ出力
CakePHPでログインや操作履歴などのアクセスログ出力処理を作成します。
-
-
CakePHP3でシェルを作成しコマンドラインから実行・CakePHP2との違い
CakePHP3のシェルスクリプトを作成し、コマンドラインから実行する方法を解説。複数単語をつなげる場合の対応方法がCakePHP2より制限が厳しくなったのでCakePHP3の命名規則の確認が必要だ。
-
-
CakePHP3でテーブルにカラム(項目)を追加したときに変更するポイントのまとめ
CakePHP3でシステム開発をする際、途中でカラムを追加した場合に何を変更すればいいかを確認。カラムを追加する前後で Bakeした結果を比較し、変更になった点をリストアップした。
-
-
CakePHPのバリデーションを入力値・項目の条件によって変える方法を解説
入力された値によってバリデーション(入力チェック)の内容を切り替える。その処理をCakePHPで実装する方法を解説。条件ごとに unset関数を使ってバリデーションを削除する、という方法を採る。
-
-
CakePHP3で環境変数を設定して本番環境と開発環境を分けて処理をする場合
CakePHP3で開発環境と本番環境とで違う設定ファイルを読み込ませて環境ごとに定数を切り替える方法を解説。Apacheのhttpd.confに環境変数を設定し、それを読み込み判別する。
-
-
CakePHPで favicon.icoやapple-touch-icon-144-precomposed.pngが could not be foundのエラーが出るときの対処方法
CakePHPで「CakeDC/Users」などルーティングを行うプラグインを利用するときに、favicon.icoやapple-touch-icon-144-precomposed.pngがNotFoundエラーになることがある。その対処方法の解説。
-
-
CakePHP3の画像、ファイルアップロードプラグインUpload Plugin 3.0の設置解説・その1
CakePHP3でファイル、画像をアップロードするプラグイン、upload plugin 3を導入する手順を解説した記事。3部作のその1で基本的な導入方法の解説で読みながら簡単に導入が可能。
-
-
CakePHP4のフラッシュメッセージの表示場所、デザインを変更する方法を解説
CakePHP4のエラーメッセージ、完了メッセージなどを表示するフラッシュ処理の解説。Controller、レイアウトファイル、テンプレートファイルでそれぞれ処理を指定する。