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
この記事が参考になったと思いましたらソーシャルメディアで共有していただけると嬉しいです!
関連記事
-
-
CakePHP3のOGPはHTMLヘルパーの$this->Html->meta()を使って設定
CakePHP3でOGPを設定する方法を解説。metaタグを編集するHTMLヘルパーを利用してOGPのタグを編集する。また、エレメントとして分割することでメンテナンス性も向上させる。
-
-
MySQL+CakePHPのdate型、datetime型項目は「2999-12-31」までしか扱えない
CakePHPの日付、日時の型であるdate型、datetime型の最大値は2999-12-31となっている。MySQLは9999年までなので違いがあるので注意が必要だ。
-
-
CakePHP3でCookieを保存、呼び出し、削除の操作・CakePHP3.7対応
CakePHP3.7でCookieを保存、取り出し、削除する方法を解説。CakePHP3でのCookieの取り扱いはバージョンごとに変更されるため、環境に合わせた方法を探す必要がある。
-
-
CakePHP 2.3 連携先のテーブルの項目で条件抽出する場合
アソシエーション(連携)している先のテーブルの項目で条件抽出する際の考え方と注意点をサンプルソースを用いて説明しています。
-
-
CakePHP3のインストールでURL rewriting……のエラーが!その解決方法解説
CakePHP3をインストールしたら URL rewriting is not properly configured.のエラーが。原因は.htaccessが有効になっていないこと。httpd.confに設定を追加すればOK。
-
-
Google Analytics APIを CakePHP3で動かしてレポートデータを取得する方法の解説
CakePHP3で Google Analytics APIからレポートデータを取得する処理の解説。PHPのサンプルソースをCakePHP3で動くように改造。加えて、ディメンションやメトリックスを条件に設定する方法なども。
-
-
CakePHPのバリデーションを入力値・項目の条件によって変える方法を解説
入力された値によってバリデーション(入力チェック)の内容を切り替える。その処理をCakePHPで実装する方法を解説。条件ごとに unset関数を使ってバリデーションを削除する、という方法を採る。
-
-
CakePHP3でWarning Error: SplFileInfo::openFile()エラーが発生した場合の対処方法
CakePHP3のキャッシュファイルのパーミッションエラー Error: SplFileInfo::openFile()が発生した場合の対応方法解説。app.phpにキャッシュファイルのパーミッション設定を行い、既存のファイルは削除。
-
-
CakePHP3のCakeDC/Usersのログイン後のリダイレクトとユーザ権限管理の設定解説
CakeDC謹製Usersプラグインの紹介。ログイン認証後にリダイレクトする先の設定方法についての解説と実運用するために必要なコツを解説。便利な仕組みも仕様の理解があって初めてうまく使いこなせる。
-
-
CakePHP4で現在処理しているコントローラー名、アクション名を取得する方法
CakePHP3で現在処理しているコントローラー名、アクション名を取得する方法を解説。複数の方法があるが、getParam()メソッドを使う方法が汎用性があって便利かも。