はじめに
PHPフレームワークのFlowはDoctrineを内包しており、DB操作にはORMを用いるのが一般的です。
createQueryBuilderも一部の処理で利用しますが、用途はSELECTだけでした。
そんなcreateQueryBuilderでUPDATEなどのテーブル更新ができることを初めて知ったため、試してみようと思います。
前提:createQueryBuilderは二種類ある
前提として、createQueryBuilderはORMとDBALの二種類存在します。
この記事では、DBALでの操作をメインに紹介します。
DBALのcreateQueryBuilderでデータ更新
知らなかったのですが、DBALのcreateQueryBuilder()はINSERT、UPDATE、DELETEもできるようです。
今まで更新はORMを使っておりcreateQueryBuilderはSELECTしかしていなかったので知りませんでしたが、まぁそりゃあできるよなという感じ。
サンプルコード
各実装のサンプルコードです。
Flowのアクション内で実装しています。
INSERT
/**
* @return void
*/
public function testCreateQueryInsertAction(): void
{
// コネクション接続、トランザクション開始など
$conn = $this->entityManager->getConnection();
$conn->beginTransaction();
$qb = $conn->createQueryBuilder();
// INSERT文
$qb->insert('tag')
->values([
'persistence_object_identifier' => ':id',
'tagid' => ':tagId',
])
->setParameter('id', Algorithms::generateUUID())
->setParameter('tagId', 'tag-' . time());
// 実行とコミット
$affectedRows = $qb->executeStatement();
$conn->commit();
$this->view->assign('value', [
'sql' => $qb->getSQL(),
'affectedRows' => $affectedRows,
]);
}
UPDATE
/**
* @param string $id 更新対象のpersistence_object_identifier
* @return void
*/
public function testCreateQueryUpdateAction(string $id): void
{
// コネクション接続、トランザクション開始など
$conn = $this->entityManager->getConnection();
$conn->beginTransaction();
$qb = $conn->createQueryBuilder();
// UPDATE文
$qb->update('tag')
->set('tagid', ':tagId')
->where('persistence_object_identifier = :id')
->setParameter('tagId', 'updated-' . time())
->setParameter('id', $id);
// 実行とコミット
$affectedRows = $qb->executeStatement();
$conn->commit();
$this->view->assign('value', [
'sql' => $qb->getSQL(),
'affectedRows' => $affectedRows,
]);
}
DELETE
/**
* @param string $id 削除対象のpersistence_object_identifier
* @return void
*/
public function testCreateQueryDeleteAction(string $id): void
{
// コネクション接続、トランザクション開始など
$conn = $this->entityManager->getConnection();
$conn->beginTransaction();
$qb = $conn->createQueryBuilder();
// DELETE文
$qb->delete('tag')
->where('persistence_object_identifier = :id')
->setParameter('id', $id);
// 実行とコミット
$affectedRows = $qb->executeStatement();
$conn->commit();
$this->view->assign('value', [
'sql' => $qb->getSQL(),
'affectedRows' => $affectedRows,
]);
}
Flowで使うときの注意点
DBAL の QueryBuilder は executeStatement() の時点で即座に SQL を実行します。Connection はデフォルトで auto-commit モードのため、トランザクションを張っていなければその場でコミットされます。
以下のコードのように、トランザクションを張らずに実行するとINSERTが即時コミットされ、その後のExceptionでもロールバックされません。
public function testCreateQueryInsertAction(): void
{
// コネクション接続(トランザクション開始しない)
$conn = $this->entityManager->getConnection();
//$conn->beginTransaction();
$qb = $conn->createQueryBuilder();
// INSERT文
$qb->insert('tag')
->values([
'persistence_object_identifier' => ':id',
'tagid' => ':tagId',
])
->setParameter('id', Algorithms::generateUUID())
->setParameter('tagId', 'tag-' . time());
// 実行
$affectedRows = $qb->executeStatement();
// Exception
throw new Exception();
$this->view->assign('value', [
'sql' => $qb->getSQL(),
'affectedRows' => $affectedRows,
]);
}
FlowではpersistenceManagerを使いORMでデータを操作するのがメインだと思いますが、混ぜる場合は更新タイミングが分かれるという点に注意しましょう。
/**
* @param string $marker 確認用に tagid の先頭へ付ける文字列
* @param bool $throw falseにすると例外を投げない(比較用)
* @return void
*/
public function testTransactionAction(string $marker, bool $throw = true): void
{
// QueryBuilder: トランザクションなしでINSERT
$conn = $this->entityManager->getConnection();
$conn->createQueryBuilder()
->insert('tag')
->values([
'persistence_object_identifier' => ':id',
'tagid' => ':tagId',
])
->setParameter('id', Algorithms::generateUUID())
->setParameter('tagId', $marker . '-qb')
->executeStatement();
// ORM: persistenceManagerに追加するだけ(flushはリクエスト終了時)
$this->persistenceManager->add(new Tag($marker . '-orm'));
if ($throw) {
throw new Exception();
}
$this->view->assign('value', [
'marker' => $marker,
'thrown' => false,
]);
}
実行結果です。QueryBuilderで登録したものだけcommitされています。
mysql> select * from tag order by tagid;
+--------------------------------------+---------+
| persistence_object_identifier | tagid |
+--------------------------------------+---------+
| ea1f7bed-5e51-4ad4-955d-6d36939b5a6b | test-qb |
+--------------------------------------+---------+
1 row in set (0.00 sec)
ORMのcreateQueryBuilderも試す
前述の通り、createQueryBuilderはORMとDBALの二種類あります。
ORMはDBALとは違い、
- UPDATEとDELETEのみ利用可能
- テーブル名ではなくエンティティのクラス名で表す
という特徴があります。
トランザクション周りについても同様で、明示的に張らないと即時コミットされます。
wrapInTransactionでトランザクションを張ることは可能ですが、引数のクロージャの実行が完了した時点でコミットされる点には注意が必要です。
UPDATE
public function testOrmQueryBuilderUpdateAction(string $tagId): void
{
$qb = $this->entityManager->createQueryBuilder();
// UPDATE文(DQL)
$qb->update(Tag::class, 't')
->set('t.tagId', ':newTagId')
->where('t.tagId = :tagId')
->setParameter('newTagId', 'orm-updated-' . time())
->setParameter('tagId', $tagId);
// wrapInTransactionで囲むと、クロージャ内で例外が起きた場合はロールバックされる
$query = $qb->getQuery();
$affectedRows = $this->entityManager->wrapInTransaction(function () use ($query) {
$affectedRows = $query->execute();
return $affectedRows;
});
$this->view->assign('value', [
'dql' => $qb->getDQL(),
'sql' => $query->getSQL(),
'affectedRows' => $affectedRows,
]);
}
DELETE
public function testOrmQueryBuilderDeleteAction(string $tagId): void
{
$qb = $this->entityManager->createQueryBuilder();
// DELETE文(DQL)
$qb->delete(Tag::class, 't')
->where('t.tagId = :tagId')
->setParameter('tagId', $tagId);
// wrapInTransactionで囲むと、クロージャ内で例外が起きた場合はロールバックされる
$query = $qb->getQuery();
$affectedRows = $this->entityManager->wrapInTransaction(function () use ($query) {
$affectedRows = $query->execute();
return $affectedRows;
});
$this->view->assign('value', [
'dql' => $qb->getDQL(),
'sql' => $query->getSQL(),
'affectedRows' => $affectedRows,
]);
}
おわりに
今回はDoctrineのcreateQueryBuilderでデータ更新を試してみました。
明示的にトランザクションを張らないといけないのはそれはそうという感じですが、FlowではORMのflushを自動でしてくれるため忘れがちになりそうです。使う場合は注意して使おうと思います。
ここまでご覧いただきありがとうございました!