エス技研

WordPress、CakePHP、PHP、baserCMSなどの Web系システムを中心に情報を提供します!


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」に関する記事一覧
 
 

 - CakePHP 3.x 4.x 5.x

最後までお読みいただきましてありがとうございます。
この記事が参考になったと思いましたらソーシャルメディアで共有していただけると嬉しいです!

Message

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

下記の空欄を埋めてください。 * Time limit is exhausted. Please reload CAPTCHA.

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

※入力いただいたコメントは管理者の承認後に掲載されます。

  関連記事

CakePHP 2.3 Model、Controllerの見たい変数の中身をログ出力

CakePHPの Modelや Controllerの変数の中身をログとして出力して見る方法を提供します。

CakePHPで Auto Incrementを外すと Duplicate entry '0' for keyのエラーが出るかも
CakePHPで Auto Incrementを外すと Duplicate entry ‘0’ for keyのエラーが出るかも

CakePHPでAuto Incrementの設定を変更したときに「Duplicate entry ‘0’ for key」のエラーが出た。原因はModel内で IDを編集する処理の追加を忘れていたからだった。

CakePHP3のdatetime型カラムの日時の扱い。秒まで正しく表示する方法
CakePHP3、CakePHP4のdatetime型カラムの日時の扱い。秒まで表示する方法

CakePHP3の日時カラムで秒まで扱う場合はdate()、strtotime()関数ではうまくいかない。CakePHP3であらかじめ用意された「i18nFormat()」を使用する。

CakePHP3で生の SQLの実行はConnectionManagerを使う
CakePHP3で生の SQLの実行はConnectionManagerを使う

CakePHP3で生の SQL文を実行する方法を解説。クリエビルダーを使う場合は TableRegistryを利用するが、SQLを実行する場合は ConnectionManagerを使う。プリペアードステートメントの使用方法も解説。

CakePHP3でデータを保存する save()で発生するエラーを確認する方法を解説
CakePHP3でデータを保存する save()で発生するエラーを確認する方法を解説

CakePHP3でデータ保存処理のログを取得する方法。save()では true、falseの戻り値しか取得できないが、saveOrFail()と try…catch文を使いエラーログ、エンティティを取得し、不具合の解析を行う。

CakePHP3でDocumentRootやtmp、webroot、logsなどのフォルダへのパスの定数
CakePHP3でDocumentRootやtmp、webroot、logsなどのフォルダへのパスの定数

CakePHP3で特定フォルダのパスの定数を解説。root、DocumentRoot、app、config、webroot、tests、tmp、cache、vendor、コア、コアの srcが設定済み。realpath()関数を使うと柔軟なパス指定が可能。

CakePHP3のCakeDC/Usersのログイン後のリダイレクトを設定解説
CakePHP3のCakeDC/Usersのログイン後のリダイレクトとユーザ権限管理の設定解説

CakeDC謹製Usersプラグインの紹介。ログイン認証後にリダイレクトする先の設定方法についての解説と実運用するために必要なコツを解説。便利な仕組みも仕様の理解があって初めてうまく使いこなせる。

CakePHP 2.3でファイルをアップロード・その2 ファイル名を乱数で設定

CakePHPのアップロードするファイル名を乱数で変更しセキュリティを高める方法を解説。

CakePHP3でモデルなしフォームからCSVをアップロードしレコードを更新する方法解説
CakePHP3でモデルなしフォームからCSVをアップロードしレコードを更新する方法解説

CakePHP3でCSVファイルをアップロードしレコードを追加、更新する処理の作成方法の解説。モデルとは直接関連しないフォームからCSVファイルをアップロードするため汎用的に使用可能。

CakePHP3にWYSIWYGエディタのCKEditor4を設置、カスタマイズ方法を解説
CakePHP3にWYSIWYGエディタのCKEditor4を設置、カスタマイズ方法を解説

WYSIWYGエディタであるCKEditor4をCDNを利用して簡単にCakePHP3に導入する方法とカスタマイズする方法を解説。CakePHP3にはページごとの振り分けを行うブロック化を利用する。