(本記事は書きかけで随時更新しております。)
ORMで保存した場合のトランザクションの挙動
CakePHPのsave() はデフォルトで単一トランザクションとなる。save()オプションで無効にすることも可能。
エンティティへのデータのセット方法で、自動でvalidateが行われるかどうか異なることに注意。
https://book.cakephp.org/5/en/orm/validation.html
newEntity(データ) の場合は、この時自動でvalidationが行われる。
しかし、newEmptyEntity() で作成したエンティティのプロパティに値をセットしても自動的にvalidationされない。
例) $entity->a = null; // notEmptyのルールがあっても無視され、$entity->getErrors() == null となる。
そのため、save($entity) は成功する。それを避けるには、
1.newEntity()を使う。
2.save()の前に、validate() を明示的に起動する。
3.validationではなく、save()時に起動する、buildRules を使う。
単純なsave
save() メソッドは、デフォルトでトランザクションを利用する。
// $this->io は、ConsoleIo $io のコピー protected function simpleSave() : void { $this->io->info("CakePHP ORM save()のテスト。\n最初にtruncate table 実行し、1レコードを保存。"); $this->io->hr(); /** @var Cake\Database\Connection */ $connection = ConnectionManager::get('default'); $ret = $connection->execute( 'truncate table orders;' ); $status = $connection->inTransaction(); // トランザクションの状態をCakeは保存している。 $this->io->info("トランザクション中? " . ($status ? 'はい' : 'いいえ')); try{ $ordersTable = $this->fetchTable('Orders'); $data = ['order_name'=>'test1']; $newOrder = $ordersTable->newEntity($data); if ( $ordersTable->save($newOrder) ) { $this->io->success('save() 成功'); }else{ // validation , buildRule が失敗すると save()==false となる。 $this->io->error('save() 失敗'); } }catch(Exception $ex){ $this->io->error('Exception in orders 発生'); $this->io->error($ex->getMessage()); }finally{ } $status = $connection->inTransaction(); $this->io->info("トランザクション中? " . ($status ? 'はい' : 'いいえ')); $this->io->hr(); $orderRowCount = $ordersTable->find()->count(); $this->io->warning('Ordersのレコード数:'.$orderRowCount);
debug: connection= role= duration=0 rows=0 BEGIN debug: connection= role=write duration=0.2 rows=1 INSERT INTO orders (order_name, created, modified) VALUES ('test1', '2024-12-01 00:00:00', '2024-12-01 00:00:00') debug: connection= role= duration=0 rows=0 COMMIT
結果:確かにトランザクションで囲まれている。save()は複数レコード、複数テーブルに同時に保存することができるため、デフォルトでトランザクションが有効になると思われる。
単一テーブルに複数行を保存。 saveMany() 使用
Saving Data - 5.x
// $this->io は、ConsoleIo $io のコピー protected function multiSave(): void { $this->io->info("CakePHP ORM saveMany()のテスト。\n最初にtruncate table 実行し、2レコードを一度に保存。"); $this->io->hr(); /** @var Cake\Database\Connection */ $connection = ConnectionManager::get('default'); $ret = $connection->execute('truncate table orders;'); $status = $connection->inTransaction(); // トランザクションの状態をCakeは保存している。 $this->io->info("トランザクション中? " . ($status ? 'はい' : 'いいえ')); try { $ordersTable = $this->fetchTable('Orders'); $data = [['order_name' => 'test1'], ['order_name' => 'test2']]; $newOrder = $ordersTable->newEntities($data); if ($ordersTable->saveMany($newOrder)) { $this->io->success('save() 成功'); } else { // validation , buildRule が失敗すると save()==false となる。 $this->io->error('save() 失敗'); } } catch (Exception $ex) { $this->io->error('Exception in orders 発生'); $this->io->error($ex->getMessage()); } finally { } $status = $connection->inTransaction(); $this->io->info("トランザクション中? " . ($status ? 'はい' : 'いいえ')); $this->io->hr(); $orderRowCount = $ordersTable->find()->count(); $this->io->warning('Ordersのレコード数:' . $orderRowCount); }
debug: connection= role= duration=0 rows=0 BEGIN debug: connection= role=write duration=0.1 rows=1 INSERT INTO orders (order_name, created, modified) VALUES ('test1', '2024-12-01 00:00:00', '2024-12-01 00:00:00') debug: connection= role=write duration=0 rows=1 INSERT INTO orders (order_name, created, modified) VALUES ('test2', '2024-12-01 00:00:00', '2024-12-01 00:00:00') debug: connection= role= duration=0 rows=0 COMMIT
結果:トランザクションで囲まれている。
アソシエーションされた親子テーブルに save()で一度に保存
ordersテーブルとorder_detailsテーブルは1:Nの関係で、OrdersTable.php 内で以下の様に定義している。
// src/Model/Table/OrdersTable.php $this->hasMany('OrderDetails', [ 'foreignKey' => 'order_id', ]);
protected function multiTableSave(): void { $this->io->info("CakePHP ORM HasManyでのsave()のテスト。\n最初にtruncate table 実行し、親テーブル1レコードと子テーブル2レコードを一度に保存。"); $this->io->hr(); /** @var Cake\Database\Connection */ $connection = ConnectionManager::get('default'); $ret = $connection->execute('truncate table orders;'); $ret = $connection->execute('truncate table order_details;'); $status = $connection->inTransaction(); // トランザクションの状態をCakeは保存している。 $this->io->info("トランザクション中? " . ($status ? 'はい' : 'いいえ')); $ordersTable = $this->fetchTable('Orders'); $orderDetailsTable = $this->fetchTable('OrderDetails'); try { $data = [ 'order_name' => 'test1', 'order_details' => [ ['sub_order_name'=>'sub_name1'], ['sub_order_name'=>'sub_name2'], ], ]; $newOrder = $ordersTable->newEntity($data); if ($ordersTable->save($newOrder)) { $this->io->success('save() 成功'); } else { // validation , buildRule が失敗すると save()==false となる。 $this->io->error('save() 失敗'); } } catch (Exception $ex) { $this->io->error('Exception in orders 発生'); $this->io->error($ex->getMessage()); } finally { } $status = $connection->inTransaction(); $this->io->info("トランザクション中? " . ($status ? 'はい' : 'いいえ')); $this->io->hr(); $orderRowCount = $ordersTable->find()->count(); $this->io->warning('Ordersのレコード数:' . $orderRowCount); $orderDetailRowCount = $orderDetailsTable->find()->count(); $this->io->warning('OrderDetailsのレコード数:' . $orderDetailRowCount); }
debug: connection= role= duration=0 rows=0 BEGIN debug: connection= role=write duration=0.2 rows=1 INSERT INTO orders (order_name, created, modified) VALUES ('test1', '2024-12-01 00:00:00'', '2024-12-01 00:00:00') debug: connection= role=write duration=0.1 rows=1 INSERT INTO order_details (order_id, sub_order_name, created, modified) VALUES (1, 'sub_name1', '2024-12-01 00:00:00'', '2024-12-01 00:00:00') debug: connection= role=write duration=0 rows=1 INSERT INTO order_details (order_id, sub_order_name, created, modified) VALUES (1, 'sub_name2', '2024-12-01 00:00:00'', '2024-12-01 00:00:00') debug: connection= role= duration=0 rows=0 COMMIT
結果:トランザクションで囲まれている。