これはユアマイスター Advent Calendar 2018の17日目の記事です。
サンプルコードを元にトランザクション処理を解説します。
##サンプルコード
<?php
namespace App\Controller;
use App\Controller\AppController;
use Cake\Datasource\ConnectionManager;
/**
* Test Controller
*/
class TestController
{
public function sample()
{
if (!$this->request->is('post') && empty($this->request->data)) {
$this->Flash->error("不正なリクエストです");
return $this->redirect($this->referer());
}
$connection = ConnectionManager::get('default');
$connection->begin();
try {
$hogeUpdate = $this->Hoge->query()
->update()
->set([
'status_id' => 3,
])
->where(['hoge.id' => $this->request->data()]);
if (!$hogeUpdate->connection->execute()) {
throw new Exception();
}
$hugaUpdate = $this->Huga->query()
->update()
->set([
'flg' => 1,
])
->where(['Huga.user_id' => $this->Auth->user()]);
if (!$hugaUpdate->connection->execute()) {
throw new Exception();
}
$connection()->commit();
} catch (Exception $e) {
$this->Flash->error('exception');
$connection->rollback();
return $this->render('hoge');
}
}
}
TestControllerクラスにsampleというアクションを作成しました。
コード上$this->request->data()
と$this->Auth->user()
があるので、前提としては、ログインユーザーがある処理をした際にこのsampleアクションにPOSTされる
、という形です。
##処理の大まかな流れ
- POSTデータか判定
- トランザクションを張る
-
Hoge
とHuga
テーブルのある値を更新。 - これらの処理をコミットする
- 更新処理に失敗したら例外を投げ、View側にFlashメッセージを表示、トランザクション内の処理をrollbackし、ある画面に遷移する
##解説
ここからはトランザクションをメインに解説します。
DBの値を更新する処理の解説は致しません。
###トランザクションとは
トランザクション処理は、既知の一貫した状態のデータベースを維持するよう設計されており、相互依存のある複数の操作が全て完了するか、全てキャンセルされることを保証する。
トランザクション処理では、データベースの個々の操作が自動的に1つに連結され、不可分のトランザクションとされることがある。トランザクション処理システムは、1つのトランザクション内の全操作がエラー無しに成功するか、全操作が実行されないことを保証する。一部の操作が成功し、他の操作でエラーが発生した場合、トランザクション処理システムはそのトランザクションの「全」操作を「ロールバック; roll back」し、そのトランザクションによる痕跡を消去してデータベースを一貫した状態(そのトランザクションを開始する前の状態)にリストアする。あるトランザクションの全操作が完了した場合、そのトランザクションはシステムによって「コミット; commit」され、データベースに加えられた更新内容が恒久的なものとなる。コミットされたトランザクションがロールバックされることはない。
引用元:Wikipedia
長々と引用申し訳ありません。
つまりトランザクションとは一つの処理のまとまりを表しています。
先ほど処理の流れで
- POSTデータか判定
- トランザクションを張る
-
Hoge
とHuga
テーブルのある値を更新。 - これらの処理をコミットする
- 更新処理に失敗したら例外を投げ、View側にFlashメッセージを表示、トランザクション内の処理をrollbackし、ある画面に遷移する
と書きましたが、この2~4までが1トランザクションとなります。
この処理が引用元でも記載されているように、相互依存のある複数の操作が全て完了するか、全てキャンセルされることを保証する
という部分に当たります。
サンプルコードでは1トランザクション内で複数のテーブルの値を更新しています。
Hoge
テーブルの値の更新に成功したとしても、Huga
テーブルのテーブルの更新に失敗した場合、この1トランザクション内の処理自体がキャンセルされ、ロールバックされます。
つまりHogeテーブルの値は、更新前の状態になります。
####なんでトランザクションを張るのか?
端的にいうとデータの整合性を担保する為です。
サンプルコードを元に話しますと、Hogeテーブルのstatus_id
を3
に更新し、Hugaテーブルのflg
を1
に更新しています。
これらの更新処理は1トランザクションであり、Hogeテーブルのstatus_id
が3
であるならば、Hugaテーブルのflg
は1
である事がデータの整合性が取れている、と言えるとします。
もし、Hogeテーブルのstatus_id
が3
で、Hugaテーブルのflg
が0
の場合、データの整合性は取れていない事になります。
トランザクションを貼る事で、もし一方の処理に不具合が発生した場合、トランザクション内の処理をロールバック、つまり処理を行う前の状態に戻してくれます。(コード的にはロールバックしてくれる、というよりトランザクション内の処理に不具合が発生した場合、ロールバックするように実装する、という方が正しいですが。)
このトランザクションの仕組みがデータの整合性を担保してくれると言えます。
参考:データベースのトランザクション処理とは:不可分なひとつながりの処理|データ分析用語を解説
こちらのデータベースのトランザクション処理を図示した場合
という部分が可視化されていてわかりやすいので、掲載致します。
##CakePHPのConnectionクラス
Connectionクラスには上記で話してきたトランザクションの機能を提供しています。
なので、Connectionクラスを使用してトランザクションを張ります。
サンプルコードを例に記載します。
まずuse Cake\Datasource\ConnectionManager;
をしてエイリアスを作成します。
ConnectionManager::get('default')
で接続します。
defaultを引数に指定する事で、設定ファイルに記載した接続情報を参照します。
$connection->begin();
はSQLのBEGIN構文と同じで、開始を宣言します。
if (!$hogeUpdate->connection->execute()) {
throw new Exception();
}
こちらの処理で更新処理に失敗したら例外を投げる用にしてます
execute()
は発行したクエリの実行をするメソッドです
$connection()->commit();
はSQLのCOMMIT構文と同様で、トランザクション内の処理をコミットして永続的なものとします。
$connection->rollback();
は、SQLのROLLBACK構文と同様で、トランザクション内の処理をロールバックして取り消します。
今回のサンプルコードではORMを使用してクエリを発行していますが、execute()
メソッド内でMySQLのクエリを書くことも可能です。
$hogeUpdate = $this->Hoge->query()
->update()
->set([
'status_id' => 3,
])
->where(['hoge.id' => $this->request->query()]);
例えばこちらの処理の場合、
$connection->execute('UPDATE hoge SET status_id = ? WHERE id = ?', [
3,
$this->request->query['status_id']
]);
このような形に書き換えられます。
?
の順番とexecute()
の第二引数の連想配列の順番は対応しているので、status_id
には3
が入る、みたいな形になります。
##参考
https://book.cakephp.org/3.0/ja/orm/database-basics.html#connection
https://book.cakephp.org/3.0/ja/orm/database-basics.html#id11
https://book.cakephp.org/3.0/ja/orm/database-basics.html#database-configuration
https://dev.mysql.com/doc/refman/5.6/ja/commit.html