はじめに
トランザクションについて教科書的な話は理解しているものの、MariaDB+PHP+CakePHP5での動きをもっと理解したいと思い、いろいろテストしてみた。理解と異なる点もあったので随時メモしていく。
本記事はわかっている人には特に目新しいものではないと思う。
目的
分離レベルやロック等の実験もしたいが、まずは単純に、commit/rollbackの動きをいろんなパターンで確認する。
環境
Xserver Standard plan上
MariaDB. ※Xserverは、サーバーにもよるが、PHP,MariaDBは同じサーバー上にある。
PHP8.3.10
CakePHP5.1
テストDB
何でもよいが、CakePHPの命名規約に合わせてある。
-- 注文テーブル CREATE TABLE `orders` ( `id` int(11) NOT NULL, `order_name` varchar(100) NOT NULL COMMENT '注文名', `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='注文トランザクションテスト用'; ALTER TABLE `orders` ADD PRIMARY KEY (`id`); ALTER TABLE `orders` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; -- 注文詳細テーブル CREATE TABLE `order_details` ( `id` int(11) NOT NULL, `order_id` int(11) NOT NULL COMMENT '注文番号', `sub_order_name` varchar(100) DEFAULT NULL COMMENT 'サブ注文名', `created` datetime DEFAULT NULL, `modified` datetime DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='注文明細トランザクションテスト用'; ALTER TABLE `order_details` ADD PRIMARY KEY (`id`); ALTER TABLE `order_details` MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
2つのレコードをordersに挿入するテスト
素のSQL(mysql CLI で実行)
1. 正しいSQL 最後はrollback
-- truncate table 済み -- SQL文に間違いなし MariaDB [devdb]> BEGIN;INSERT INTO `orders` (`order_name`) VALUES ('name1');INSERT INTO `orders` (`order_name`) VALUES ('name2');ROLLBACK; Query OK, 0 rows affected (0.000 sec) Query OK, 1 row affected (0.004 sec) Query OK, 1 row affected (0.000 sec) Query OK, 0 rows affected (0.000 sec) MariaDB [devdb]> select * from orders; Empty set (0.000 sec)
結果:正常
2. ステートメントエラーとなるSQL 最後はrollback
-- truncate table 済み -- SQL文(フィールド名)に間違い MariaDB [devdb]> BEGIN;INSERT INTO `orders` (`order_name`) VALUES ('name1');INSERT INTO `orders` (`order_names`) VALUES ('name2');ROLLBACK; Query OK, 0 rows affected (0.000 sec) Query OK, 1 row affected (0.003 sec) ERROR 1054 (42S22): Unknown column 'order_names' in 'field list' Query OK, 0 rows affected (0.000 sec) MariaDB [devdb]> select * from orders; Empty set (0.000 sec)
結果:正常
3.正しいSQL 最後はcommit
MariaDB [devdb]> BEGIN;INSERT INTO `orders` (`order_name`) VALUES ('name1');INSERT INTO `orders` (`order_name`) VALUES ('name2');commit; Query OK, 0 rows affected (0.000 sec) Query OK, 1 row affected (0.000 sec) Query OK, 1 row affected (0.000 sec) Query OK, 0 rows affected (0.000 sec) MariaDB [devdb]> select * from orders; +----+------------+---------+----------+ | id | order_name | created | modified | +----+------------+---------+----------+ | 1 | name1 | NULL | NULL | | 2 | name2 | NULL | NULL | +----+------------+---------+----------+ 2 rows in set (0.001 sec)
結果:正常
4. ステートメントエラーとなるSQL 最後はcommit
MariaDB [devdb]> BEGIN;INSERT INTO `orders` (`order_name`) VALUES ('name1');INSERT INTO `orders` (`order_names`) VALUES ('name2');commit; Query OK, 0 rows affected (0.000 sec) Query OK, 1 row affected (0.007 sec) ERROR 1054 (42S22): Unknown column 'order_names' in 'field list' Query OK, 0 rows affected (0.000 sec) MariaDB [devdb]> select * from orders; +----+------------+---------+----------+ | id | order_name | created | modified | +----+------------+---------+----------+ | 1 | name1 | NULL | NULL | +----+------------+---------+----------+ 1 row in set (0.000 sec)
結果:エラーとならなかった行はcommitされる
5. 入れ子となるトランザクション内でステートメントエラーとなるSQL
MariaDB [devdb]> BEGIN;INSERT INTO `orders` (`order_name`) VALUES ('name1');BEGIN;INSERT INTO `order_details` (`order_id`,`sub_order_names`) VALUES ('1','sub_name1');ROLLBACK;ROLLBACK; Query OK, 0 rows affected (0.000 sec) Query OK, 1 row affected (0.000 sec) Query OK, 0 rows affected (0.000 sec) ERROR 1054 (42S22): Unknown column 'sub_order_names' in 'field list' Query OK, 0 rows affected (0.000 sec) Query OK, 0 rows affected (0.000 sec) MariaDB [devdb]> select * from orders; +----+------------+---------+----------+ | id | order_name | created | modified | +----+------------+---------+----------+ | 1 | name1 | NULL | NULL | +----+------------+---------+----------+ 1 row in set (0.000 sec) MariaDB [devdb]> select * from order_details; Empty set (0.000 sec)
結果:mariaDBで入れ子はできない。次のbeginでコミットされrollbackは無意味となる。SAVEPOINTを使う。
CakePHP内ステートメントオブジェクト のSQL文内で実行
CakePHP のcake コマンドを拡張するCommandオブジェクトを使ってCLIで実施。
上記 1-5 同様のSQLを実行
/** @var Cake\Database\Connection */ $connection = ConnectionManager::get('default'); $ordersTable = $this->fetchTable('Orders'); $ret = $connection->execute( 'truncate table orders;' ); // 以下、execute で直接SQLを実行。
結果:mysql CLI と同じ結果
ConnectionManager のbeginを利用してトランザクションを開始し、execute()メソッドを使用
わざとステートメントエラーとなる。SQL エラーとなったらrollbackする。
// $this->io は、ConsoleIo $io のコピー $this->io->info("テスト 開始"); $this->io->out("トランザクション内で2レコードを挿入する。\n2レコード目でわざと失敗させる。\n最初にtruncate table 実行する。"); $this->io->hr(); /** @var Cake\Database\Connection */ $connection = ConnectionManager::get('default'); $ordersTable = $this->fetchTable('Orders'); $ret = $connection->execute( 'truncate table orders;' ); $connection->begin(); $success = false; try{ $this->io->out("ordersに挿入(成功SQL実行)"); $ret = $connection->execute( 'INSERT into orders (order_name) values(?)', ["name1"] ); // わざと失敗 $this->io->out("ordersに挿入(失敗SQL実行)"); $ret = $connection->execute( 'INSERT into orders (order_names) values(?)', ["name2"] ); $connection->commit(); $success = true; }catch(Exception $ex){ $this->io->error('Exception 発生'); $this->io->error($ex->getMessage()); $connection->rollback(); $success = false; }finally{ $connection = null; } $this->io->hr(); $rowCount = $ordersTable->find()->count(); $this->io->warning('Ordersのレコード数:'.$rowCount); if ( $success){ $this->io->warning('db insert は成功→テスト失敗'); }else{ $this->io->warning('db insert は失敗→テスト成功'); }
実行結果
> bin/cake transaction test トランザクションのテスト開始 テスト 開始 トランザクション内で2レコードを挿入する。 2レコード目でわざと失敗させる。 最初にtruncate table 実行する。 ------------------------------------------------------------------------------- ordersに挿入(成功SQL実行) ordersに挿入(失敗SQL実行) Exception 発生 SQLSTATE[42S22]: Column not found: 1054 Unknown column 'order_names' in 'field list' ------------------------------------------------------------------------------- Ordersのレコード数:0 db insert は失敗→テスト成功
結果:正常
もし、catch 節の、$connection->rollback(); を消すと、オートコミットとなり、正常な一行目はコミットされる。