CakePHP4の規約外のカラムをキーにアソシエーション(テーブル連結)する方法
2024/06/21
CakePHPの規約外のカラムをキーにアソシエーション(テーブル連結)する方法
CakePHP4で CakePHPの規定から外れることでデフォルトではアソシエーション(テーブルの関連付け)されないカラム(項目)をキーとしてアソシエーションする方法を解説します。
前提:存在するテーブルのイメージ
解説する環境には下記の 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」。
「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のFrozenDateで1ヵ月前、先月、今月1日、来月末の日付などを算出する方法CakePHP4のcake cache clear_allでPermission deniedはパーミッションの変更が必要
CakePHP4のクリエビルダーを使用してOR条件をAND条件でつなぐSQL文を作る方法
CakePHP4のController内でViewテンプレート、レイアウトの変更設定を記述する方法
CakePHP4から外部のデータベースにアクセスする方法解説
CakePHP4の数値項目は「like %10%」の部分一致検索(find select)はできない
CakePHP4でロギングスコープやログレベルを使用してログを出し分ける方法を解説
CakePHP2、CakePHP3、CakePHP4、CakePHP5のバージョンを調べる 2つの方法
Windows上のXAMPP環境のCakePHPのコマンド実行時に環境変数を指定する方法
CakePHP4で複数の引数(パラメータ)を付与してコマンドを実行する方法
その他の「CakePHP4」に関する記事一覧
GoogleAdwords
GoogleAdwords
この記事が参考になったと思いましたらソーシャルメディアで共有していただけると嬉しいです!
関連記事
-
CakePHP3のプラグイン「CakeDC/Users」を日本語化・翻訳ファイルもダウンロード可
CakePHP3のユーザ管理、ログイン認証プラグインである「CakeDC/Users」のメッセージを日本語にする手順の解説とともに、日本語の翻訳ファイルを提供。ファイルを設置すれば日本語になる!
-
CakePHP4のCakeDC/Usersの Usersへの接続、バリデーションのカスタマイズ方法解説
CakeDC謹製Usersプラグインの紹介。CakePHP4で使う場合のUsersのカスタマイズとして入力項目のバリデーションの変更を、プラグインのファイルは触らずオーバーライドにより実装する方法を解説する。
-
CakePHP3ログファイルへの出力・$this->log()、独自ログへの出力方法の解説
コントロール、モデルの変数の中身を見るときはログに出力する方法が有効です。$this->log()を利用すると変数だけじゃなく、連想配列、オブジェクトも簡単にログ出力ができます。
-
CakePHP4のFrozenDateで1ヵ月前、先月、今月1日、来月末の日付などを算出する方法
CakePHPには「FrozenDate」の日付を扱う関数が用意されている。これを利用して、1ヶ月後、月末日、月初日、5日後などを指定して日付を取得できる。それを解説。
-
CakePHP3のCakeDC/Usersのバリデーションのカスタマイズ方法解説
CakeDC謹製Usersプラグインの紹介。Usersのカスタマイズとして入力項目のバリデーションの変更を、プラグインのファイルは触らずオーバーライドにより実装する方法を解説する。
-
CakePHP3でCookieを保存、呼び出し、削除の操作・CakePHP3.7対応
CakePHP3.7でCookieを保存、取り出し、削除する方法を解説。CakePHP3でのCookieの取り扱いはバージョンごとに変更されるため、環境に合わせた方法を探す必要がある。
-
CakePHP 2.3 Model、Controllerの見たい変数の中身をログ出力
CakePHPの Modelや Controllerの変数の中身をログとして出力して見る方法を提供します。
-
Windows環境の XAMPPを利用して CakePHPの開発する際の注意点
WindowsベースにXAMPPで環境を構築しCakePHP4を利用したWebシステムを構築する際は、大文字と小文字の違いを意識する必要がある。LinuxベースのWebサーバに移動させたときに不具合で動作しないこともある。
-
CakePHP 2.3 コマンドラインからPHPのシェル実行の方法解説
CakePHP 2.3でコマンドラインから CakePHPで記述した処理を実行する方法を解説します。
-
CakePHP3でWarning Error: SplFileInfo::openFile()エラーが発生した場合の対処方法
CakePHP3のキャッシュファイルのパーミッションエラー Error: SplFileInfo::openFile()が発生した場合の対応方法解説。app.phpにキャッシュファイルのパーミッション設定を行い、既存のファイルは削除。