はじめに
お仕事で、Zend Frameworkのバージョンアップをしなければならなくなった・・と思ったら、Zend Frameworkはもうなくて、Laminas Projectに移って新たなフレームワークとして公開されている。
いちから勉強しないといけないじゃん、ということで、必要な要件を満たせるかどうかを一歩ずつ調査していく。
要件(6)
6つ目の要件は、「DBのテーブルに対してJOINクエリが発行できること」。
Zend Frameworkでは、クエリビルダーがあったので、それでJOINクエリを書いていたが、Laminasでも同じようにできるのか、はたまたSQL文をベタで書く羽目になるのか、その辺りを調査していく。
導入
こちらでセットアップした環境を(コピーして)使っていく。
私は以下のようにコピーを作成。
cp -pr laminas-setup-5-multiple-databases laminas-setup-6-join-queries cd laminas-setup-6-join-queries
DBへの追加
今回、テナントDBの方がメインになるので、テナントDBの方でJOINクエリを試していく。 以下のようにテナントDBに新たにテーブルを追加する。
\c tenant_0001 tenant_0001_user CREATE TABLE messages( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, title VARCHAR(128) NOT NULL, content TEXT NOT NULL); INSERT INTO messages(user_id, title, content) VALUES(1, 'テナント1ー1さんへのメッセージ1', 'メッセージ1の内容'); INSERT INTO messages(user_id, title, content) VALUES(2, 'テナント1ー2さんへのメッセージ1', 'メッセージ1の内容'); INSERT INTO user_master(user_name, real_name) VALUES('tenant0002', 'テナント1 次郎');
\c tenant_0002 tenant_0002_user CREATE TABLE messages( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL, title VARCHAR(128) NOT NULL, content TEXT NOT NULL); INSERT INTO messages(user_id, title, content) VALUES(1, 'テナント2ー1さんへのメッセージ1', 'メッセージ1の内容'); INSERT INTO messages(user_id, title, content) VALUES(2, 'テナント2ー2さんへのメッセージ1', 'メッセージ1の内容'); INSERT INTO user_master(user_name, real_name) VALUES('tenant0001', 'テナント2 太郎');
想定するクエリ
先に追加したテーブルを使って、「実名に"太郎"を含むユーザーのメッセージ一覧」を取得するクエリを書いてみる。 SQL文で書くと以下のような感じになる。
SELECT m.id, m.user_id, m.title, m.content FROM messages m JOIN user_master u ON m.user_id = u.id WHERE u.real_name LIKE '%太郎%' ORDER BY m.id LIMIT 10 OFFSET 0
設定・実装
まず、messagesテーブル用のModelクラスmodule/Application/src/Model/Tenant/Message.php
を作る。
<?php declare(strict_types=1); namespace Application\Model\Tenant; class Message { public int $id; public int $userId; public string $title; public string $content; public function exchangeArray(array $data) { $this->id = isset($data['id']) ? $data['id'] : null; $this->userId = isset($data['user_id']) ? $data['user_id'] : null; $this->title = isset($data['title']) ? $data['title'] : null; $this->content = isset($data['content']) ? $data['content'] : null; } }
次に、Tableクラスを作るのだが、以前のようにTableGatewayを介するのではなく、Adapterを直接扱う形になるので、その点だけ注意。
ファイルmodule/Application/src/Model/Tenant/MessagesTable.php
を以下のように作成する。
<?php declare(strict_types=1); namespace Application\Model\Tenant; use RuntimeException; use Laminas\Db\Adapter\Adapter; use Laminas\Db\Sql\Sql; use Laminas\Db\Sql\Predicate\Like; use Application\Model\Tenant\Message; class MessagesTable { private $adapter; public function __construct(Adapter $adapter) { $this->adapter = $adapter; } public function fetchAll() { $sql = new Sql($this->adapter); $select = $sql->select(); $select->from('messages'); $select->order('id'); $select->limit(10); $select->offset(0); $statement = $sql->prepareStatementForSqlObject($select); $results = $statement->execute(); return $this->buildMessages($results); } public function getMessages($realNameCond) { if (is_null($realNameCond)) { throw new RuntimeException('Real name condition is required.'); } $sql = new Sql($this->adapter); $select = $sql->select(); $select->columns(['id', 'user_id', 'title', 'content']); $select->from(['m' => 'messages']); $select->join(['u' => 'user_master'], 'm.user_id = u.id', [], $select::JOIN_INNER); $like = new Like('real_name', "%{$realNameCond}%"); $select->where($like); $select->order('m.id'); $select->limit(10); $select->offset(0); $statement = $sql->prepareStatementForSqlObject($select); $results = $statement->execute(); return $this->buildMessages($results); } private function buildMessages($results) { $messages = []; foreach ($results as $result) { $message = new Message(); $message->exchangeArray($result); $messages[] = $message; } return $messages; } }
続いて、サービスマネージャの設定を編集する。module/Application/src/Module.php
を以下のように編集する。
<?php declare(strict_types=1); namespace Application; use Laminas\Db\Adapter\Adapter; use Laminas\Db\Adapter\AdapterInterface; use Laminas\Db\ResultSet\ResultSet; use Laminas\Db\TableGateway\TableGateway; use Laminas\ModuleManager\Feature\ConfigProviderInterface; class Module implements ConfigProviderInterface { public function getConfig(): array { /** @var array $config */ $config = include __DIR__ . '/../config/module.config.php'; return $config; } public function getServiceConfig(): array { return [ 'factories' => [ Model\DbMaster2Table::class => function($container) { $tableGateway = $container->get(Model\DbMaster2TableGateway::class); return new Model\DbMaster2Table($tableGateway); }, Model\DbMaster2TableGateway::class => function($container) { $dbAdapter = $container->get(AdapterInterface::class); $resultSetPrototype = new ResultSet(); $resultSetPrototype->setArrayObjectPrototype(new Model\DbMaster2()); return new TableGateway('db_master_2', $dbAdapter, null, $resultSetPrototype); }, Model\Tenant\UserMasterTable::class => function($container) { $tableGateway = $container->get(Model\Tenant\UserMasterTableGateway::class); return new Model\Tenant\UserMasterTable($tableGateway); }, Model\Tenant\UserMasterTableGateway::class => function($container) { $dbAdapter = $container->get(Model\Tenant\Adapter::class); $resultSetPrototype = new ResultSet(); $resultSetPrototype->setArrayObjectPrototype(new Model\Tenant\UserMaster()); return new TableGateway('user_master', $dbAdapter, null, $resultSetPrototype); }, Model\Tenant\MessagesTable::class => function($container) { $dbAdapter = $container->get(Model\Tenant\Adapter::class); return new Model\Tenant\MessagesTable($dbAdapter); }, Model\Tenant\Adapter::class => function($container) { $tenantCode = $_GET['tenant_code']; $dbMaster2Table = $container->get(Model\DbMaster2Table::class); $result = $dbMaster2Table->getDb($tenantCode); $config = [ 'driver' => 'pdo_pgsql', 'database' => $result->dbName, 'username' => $result->dbUser, 'password' => $result->dbPassword, 'hostname' => $result->dbHost, 'port' => $result->dbPort, 'charset' => 'UTF-8', ]; $dbAdapter = new Adapter($config); return $dbAdapter; }, ], ]; } public function getControllerConfig() { return [ 'factories' => [ Controller\TenantController::class => function($container) { return new Controller\TenantController( $container->get(Model\DbMaster2Table::class), $container->get(Model\Tenant\UserMasterTable::class), $container->get(Model\Tenant\MessagesTable::class) ); }, ], ]; } }
編集内容は、Model\Tenant\MessagesTable::class
のファクトリ定義の追加と、Controller\TenantController::class
クラスのコンストラクタに渡すテーブルオブジェクトの追加。
続いて、module/Application/src/Controller/TenantController.php
の修正。
<?php declare(strict_types=1); namespace Application\Controller; use Laminas\Mvc\Controller\AbstractActionController; use Laminas\View\Model\ViewModel; use Application\Model\DbMaster2Table; use Application\Model\Tenant\UserMasterTable; use Application\Model\Tenant\MessagesTable; class TenantController extends AbstractActionController { private $dbMaster2Table; private $userMasterTable; private $messagesTable; public function __construct(DbMaster2Table $dbMaster2Table, UserMasterTable $userMasterTable, MessagesTable $messagesTable) { $this->dbMaster2Table = $dbMaster2Table; $this->userMasterTable = $userMasterTable; $this->messagesTable = $messagesTable; } public function tenantAction() { $result = $this->dbMaster2Table->getDb($_GET['tenant_code']); return new ViewModel([ 'db_name' => $result->dbName, ]); } public function usersAction() { $result = $this->userMasterTable->fetchAll(); return new ViewModel([ 'users' => $result, ]); } public function messagesAction() { $allResult = $this->messagesTable->fetchAll(); $userResult = $this->messagesTable->getMessages('太郎'); return new ViewModel([ 'all_messages' => $allResult, 'user_messages' => $userResult, ]); } }
コンストラクタへの引数の追加と、messagesAction()
メソッドの追加。
最後にビューテンプレートの作成。ファイルパスはmodule/Application/view/application/tenant/messages.tpl
。
全メッセージ <ul> {foreach from=$all_messages item='message'} <li>{$message->id|escape:"html"}:{$message->userId|escape:"html"}({$message->title|escape:"html"})</li> {/foreach} </ul> ユーザーメッセージ <ul> {foreach from=$user_messages item='message'} <li>{$message->id|escape:"html"}:{$message->userId|escape:"html"}({$message->title|escape:"html"})</li> {/foreach} </ul>
動作確認
以下のURLにアクセスして、ページが表示されるかを確認する。
http://192.168.56.xxx/laminas-setup-6-join-queries/public/tenant/messages?tenant_code=0001
以下のような画面が表示されればOK。
まとめ
- クエリビルダーのメソッドが充実しているので、たいていのクエリはそれで書けそう
- WHERE条件も、述語クラスが一通り用意されており、困ることはなさそう