【CakePHP入門】テーブルを結合(JOIN)して検索する方法

こんにちは!フリーエンジニアのせきです。

CakePHPには、関連のあるテーブルを結合して検索する方法がいくつか用意されています。

この記事では、

・関連のあるテーブルをモデルに定義する方法を知りたい
・テーブルを結合して検索する方法を知りたい

という基本的な内容から、

・複数のテーブルと結合する方法を知りたい

といった応用的な内容に関しても解説していきます。

今回はそんなCakePHPでテーブルを結合して検索する方法について、わかりやすく解説します!

目次

モデルを関連付けて検索する方法

サンプルでは、以下のような受注テーブルを作成して、一覧表示します。

テーブル定義

CREATE TABLE ACCEPT_ORDER
(
    ID INT NOT NULL AUTO_INCREMENT, -- 受注ID
    CUSTOMER_ID INT, -- 顧客ID
    PRODUCT_ID INT, -- 製品ID
    QUANTITY INT, -- 数量
    PRIMARY KEY (ID)
);

データ

IDCUSTOMER_IDPRODUCT_IDQUANTITY
1122
2221
3231
4313
5332

一覧に顧客名を表示できるよう、顧客テーブルを作成します。

テーブル定義

CREATE TABLE CUSTOMER
(
    ID INT NOT NULL AUTO_INCREMENT, -- 顧客ID
    CUSTOMER_NAME VARCHAR(32), -- 顧客名
    PRIMARY KEY (ID)
);

データ

IDCUSTOMER_NAME
1Tanaka
2Saito
3Yamada

受注テーブル(ACCEPT_ORDER)のCUSTOMER_IDと顧客テーブル(CUSTOMER)のIDが紐づくものとし、受注テーブルに顧客テーブルを結合して検索します。

受注テーブルと顧客テーブルは、1つの顧客IDに対し複数の受注データが存在するので、「多 対 1」という関係になります。

CakePHPのモデルでは、「多 対 1」の関連をbelongsToで定義します。

belongsTo(結合するモデル名);

受注テーブルのModelは以下のようになります。

[プロジェクトのパス]/src/Model/Table/AcceptOrderTable.php

namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class AcceptOrderTable extends Table
{
    public function initialize(array $config)
    {
        $this->belongsTo('Customer');
    }
}

「$this->belongsTo(‘Customer’);」で、顧客テーブルとの関連を定義しています。

テーブルの項目名を「結合するテーブル名_ID」にしておくと、自動でその項目をキーに結合します。

結合される顧客テーブルのModelには、関連の定義は不要です。

[プロジェクトのパス]/src/Model/Table/CustomerTable.php

<?php
namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class CustomerTable extends Table
{
}

Entityはそのままです。

[プロジェクトのパス]/src/Model/Entity/AcceptOrder.php

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

class AcceptOrder extends Entity
{
}

[プロジェクトのパス]/src/Model/Entity/Customer.php

<?php
namespace App\Model\Entity;

use Cake\ORM\Entity;

class Customer extends Entity
{
}

Controllerで検索します。

関連のあるテーブルを結合して検索するには、containを使用し以下のように記述します。

find('all')->contain(結合するモデル名);

受注テーブルの一覧を表示するControllerです。

[プロジェクトのパス]/src/Controller/AcceptOrderController.php

<?php
namespace App\Controller;

use App\Controller\AppController;

class AcceptOrderController extends AppController
{
    public function index()
    {
        $query =  $this->AcceptOrder->find('all')->contain(['Customer']);
        $this->set('acceptOrder', $query);
    }
}

「$this->AcceptOrder->find(‘all’)->contain([‘Customer’])」で、顧客テーブルと結合しています。

結合したテーブルの値は、以下のように取得できます。

$変数名->結合したテーブル名

結合したテーブル名は、すべて小文字で記述します。

受注テーブルの一覧を表示するTemplateです。

[プロジェクトのパス]/src/Template/AcceptOrder/index.ctp

<table cellpadding="0" cellspacing="0">
    <thead>
        <tr>
            <th scope="col">ID</th>
            <th scope="col">CUSTOMER_NAME</th>
            <th scope="col">PRODUCT_ID</th>
            <th scope="col">QUANTITY</th>
        </tr>
    </thead>
    <tbody>
        <?php foreach ($acceptOrder as $acceptOrder): ?>
        <tr>
            <td><?= $acceptOrder->ID ?></td>
            <td><?= $acceptOrder->customer->CUSTOMER_NAME ?></td>
            <td><?= $acceptOrder->PRODUCT_ID ?></td>
            <td><?= $acceptOrder->QUANTITY ?></td>
        </tr>
        <?php endforeach; ?>
    </tbody>
</table>

「$acceptOrder->customer->CUSTOMER_NAME」で、結合した顧客テーブルの顧客名を表示しています。

「http://[サーバ名]/[プロジェクト名]/acceptOrder」にアクセスすると、以下のように表示されます。
001

検索時にJOINを追加する方法

Modelには手を加えずControllerで検索する時に結合する方法もあります。

検索を行うfind()は、「->」(アロー演算子)を続けて、様々なオプションや条件を指定することができます。

ここではテーブルの結合を行うjoin()leftJoin()を解説します。

join()を使用する方法

join()には、配列で以下のような指定をします。

join([
    'table' => 結合するテーブル名,
    'alias' => テーブル別名,
    'type' => 結合方法,
    'conditions' => 結合する条件
])

結合方法には「LEFT」「RIGHT」「INNER」が指定できます。

conditionsに指定した条件で、tableに指定したテーブルと結合します。

さらに、select()を使用して、取得する項目を指定します。

select([
    別名 => モデル名.項目名,
    ・・・
])

join()を使用したControllerです。

[プロジェクトのパス]/src/Controller/AcceptOrderController.php

<?php
namespace App\Controller;

use App\Controller\AppController;

class AcceptOrderController extends AppController
{
    public function index()
    {
        $query =  $this->AcceptOrder->find()
            ->join([
                'table' => 'customer',
                'alias' => 'c',
                'type' => 'LEFT',
                'conditions' => 'c.id = AcceptOrder.customer_id',
            ])->select([
                'id' => 'AcceptOrder.id',
                'customer_name' => 'c.customer_name',
                'product_id' => 'AcceptOrder.product_id',
                'quantity' => 'AcceptOrder.quantity',
            ]);

        $this->set('acceptOrder', $query);
    }
}

テーブルの値は、以下のように取得できます。

$変数名[selectで指定した別名]

これを使用して、Templateは以下のようになります。

[プロジェクトのパス]/src/Template/AcceptOrder/index.ctp

<table cellpadding="0" cellspacing="0">
    <thead>
        <tr>
            <th scope="col">ID</th>
            <th scope="col">CUSTOMER_NAME</th>
            <th scope="col">PRODUCT_ID</th>
            <th scope="col">QUANTITY</th>
        </tr>
    </thead>
    <tbody>
        <?php foreach ($acceptOrder as $acceptOrder): ?>
        <tr>
            <td><?= $acceptOrder['id'] ?></td>
            <td><?= $acceptOrder['customer_name'] ?></td>
            <td><?= $acceptOrder['product_id'] ?></td>
            <td><?= $acceptOrder['quantity'] ?></td>
        </tr>
        <?php endforeach; ?>
    </tbody>
</table>

「http://[サーバ名]/[プロジェクト名]/acceptOrder」にアクセスすると、モデルを関連付けて検索した時と同じ画面が表示されます。

leftJoin()を使用する方法

join()で結合方法に「LEFT」を指定する場合には、leftJoin()を使うこともできます

leftJoin()には、以下のような指定をします。

leftJoin(
    [テーブル別名 => 結合するテーブル名],
    [結合する条件]
)

leftJoin()を使用したControllerです。

[プロジェクトのパス]/src/Controller/AcceptOrderController.php

<?php
namespace App\Controller;

use App\Controller\AppController;

class AcceptOrderController extends AppController
{
    public function index()
    {
        $query =  $this->AcceptOrder->find()
            ->leftJoin(
                ['c' => 'customer'],
                ['c.id = AcceptOrder.customer_id']
            )->select([
                'id' => 'AcceptOrder.id',
                'customer_name' => 'c.customer_name',
                'product_id' => 'AcceptOrder.product_id',
                'quantity' => 'AcceptOrder.quantity',
            ]);

        $this->set('acceptOrder', $query);
    }
}

Templateはjoin()の時と同じもので、同じ画面が表示されます。

複数のテーブルと結合する方法

1つのテーブルに、複数のテーブルを結合することもできます。

製品テーブルを作成し、一覧に製品名も表示するようにします。

テーブル定義

CREATE TABLE PRODUCT
(
    ID INT NOT NULL AUTO_INCREMENT, -- 製品ID
    PRODUCT_NAME VARCHAR(32), -- 製品名
    UNIT_PRICE INT, -- 値段
    PRIMARY KEY (ID)
);

データ

IDPRODUCT_NAMEUNIT_PRICE
1Bag5000
2Shoes8000
3Hat3000

Modelに関連を複数定義する場合は、belongsToを追加します。

[プロジェクトのパス]/src/Model/Table/AcceptOrderTable.php

namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

class AcceptOrderTable extends Table
{
    public function initialize(array $config)
    {
        $this->belongsTo('Product'); // 追加
        $this->belongsTo('Customer');
    }
}

Controllerのcontainにも追加します。

[プロジェクトのパス]/src/Controller/AcceptOrderController.php

<?php
namespace App\Controller;

use App\Controller\AppController;

class AcceptOrderController extends AppController
{
    public function index()
    {
        $query =  $this->AcceptOrder->find('all')->contain(['Product', 'Customer']); // 'Product'追加
        $this->set('acceptOrder', $query);
    }
}

製品IDではなく製品名を表示するよう、Templateを修正します。

[プロジェクトのパス]/src/Template/AcceptOrder/index.ctp

<table cellpadding="0" cellspacing="0">
    <thead>
        <tr>
            <th scope="col">ID</th>
            <th scope="col">CUSTOMER_NAME</th>
            <th scope="col">PRODUCT_NAME</th>
            <th scope="col">QUANTITY</th>
        </tr>
    </thead>
    <tbody>
        <?php foreach ($acceptOrder as $acceptOrder): ?>
        <tr>
            <td><?= $acceptOrder->ID ?></td>
            <td><?= $acceptOrder->customer->CUSTOMER_NAME ?></td>
            <td><?= $acceptOrder->product->PRODUCT_NAME ?></td>
            <td><?= $acceptOrder->QUANTITY ?></td>
        </tr>
        <?php endforeach; ?>
    </tbody>
</table>

「$acceptOrder->product->PRODUCT_NAME」で製品テーブルの製品名が取得できます。

「http://[サーバ名]/[プロジェクト名]/acceptOrder」にアクセスすると、製品名が表示されています。
002

Contorollerのjoin()でも、複数のテーブルを結合することができます。

以下のように、join()にテーブル別名をキーにした連想配列を指定します。

$query =  $this->AcceptOrder->find()
    ->join([
        'p' => [
            'table' => 'product',
            'type' => 'LEFT',
            'conditions' => 'p.id = AcceptOrder.product_id',
        ],
        'c' => [
            'table' => 'customer',
            'type' => 'LEFT',
            'conditions' => 'c.id = AcceptOrder.customer_id',
        ]
    ])->select([
        'id' => 'AcceptOrder.id',
        'customer_name' => 'c.customer_name',
        'product_name' => 'p.product_name',
        'quantity' => 'AcceptOrder.quantity',
    ]);

まとめ

今回はテーブルを結合して検索する方法について解説しました。

実際のシステムでは複数のテーブルでデータを管理し、そのデータをユーザが扱いやすいように結合して表示したりするので、テーブルの結合は必須です。

テーブルを結合する方法を忘れてしまったら、この記事を思い出して下さい!

この記事を書いた人

フリーランスでWebシステム開発やゲーム開発をしています。
読者の方にプログラミングの面白さをお伝えしたいです。

目次