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
この記事が参考になったと思いましたらソーシャルメディアで共有していただけると嬉しいです!
関連記事
-
CakePHPのFlashエラーは出るが入力項目ごとのメッセージが出ないエラーの原因
Bakeして自動生成した入力フォーム処理を元に少し処理を追加したら、入力エラーがあってもエラーメッセージが表示されなくなった。原因はリダイレクトの処理にあった。
-
CakePHP4 でコマンドプログラム(シェルプログラム)を作成する方法解説
CakePHP4でバッチ処理を行うためのコマンド・シェルの実装方法について解説。bakeでテンプレートファイルを作成し、「execute()」に処理を記述する方法を解説。
-
CakePHP 2.3でファイルのアップロード処理を作る
CakePHPでプラグインを使わないファイルアップロード処理を解説します。簡単です。DBにファイルを格納する方法も。
-
CakePHP 2.3 Search Pluginで検索処理 その3入力エリア一つで複数の項目を同時に検索する方法
CakePHPの検索プラグイン Search Pluginの検索処理の中で入力エリア一つで複数の項目を同時に検索する方法を解説。
-
CakePHP3のビューで受取ったテーブルのオブジェクトを連想配列に変換する方法
コントローラーからビューに送ったテーブルのオブジェクトを連想配列に変換し、ビューの中で自由に使えるようにするメソッド「toArray()」の解説。連想配列に変換できれば利用度アップ!
-
CakePHP4で複数の引数(パラメータ)を付与してコマンドを実行する方法
CakePHP4でコマンドを実行する際に引数(パラメータ)をコマンド内で受け取る処理について解説。複数個の引数にも対応する記述方法も解説。
-
CakePHP4のメッセージ日本語化の設定(国際化と地域化の機能の使い方の解説)
CakePHP4の英語のメッセージを日本語化(多言語化)する手順を解説。オリジナルのメッセージを作成する方法やプログラムで文言を追加する場合の対応なども解説。
-
CakePHPで同一テーブル内の値を比較する条件でレコードを取得する方法
CakePHPの同一テーブルにある項目の値を比較し条件に合致するレコードを取得する方法を解説。[”項目名”=>”値”]ではなく[”項目名 = 項目名”]と書くところがポイント。
-
CakePHP3でassociatedを使って関連データをまとめて保存する方法(hasOne、hasMany、belongsTo)
CakePHP3で関連データをまとめて保存する方法。hasOne、hasMany、belongsTo等の関連データはassociatedを追加することでまとめて保存することができます。
-
CakePHP4のcake cache clear_allでPermission deniedはパーミッションの変更が必要
CakePHP4でキャッシュをクリアするコマンドcake cache clear_allでPermission deniedのエラーが出るのはcakeにパーミッションが足りないとき。その対処方法を解説。