トランザクションの基本から
LaravelではDBファサードを使ってトランザクション管理を実現できます。
use DB;
DB::transaction(function()
{
DB::table('users')->update();
DB::table('posts')->delete();
});
こういう風に書くこともできる。
DB:beginTransaction();
try {
// 更新処理
if(~) {
// 例外発生
}
DB::commit();
} catch(Exception $e) {
DB::rollback();
throw $e;
}
楽観的排他制御の実装
悲観的排他制御はLaravelにメソッドが用意されているのですが、楽観的排他制御に関してはよしなにやってくれる機能がありません。自分でrevisionなりupdated_atのカラムを使って判定するしかなさそうです。
今回はupdated_atカラムを見て、ページアクセス時に取得した更新日時と、POSTした時にDBから取得した更新日時を比較して、データが更新されていないか判定することにします。
ビューにupdate_atをhiddenで埋め込み値を保持しておく
<form>
氏名:<input type="text" name="name" value="{{ $user->name }}">
メールアドレス:<input type="text" name="email" value="{{ $user->email }}">
<input type="hidden" name="id" value="{{ $user->id}}">
<input type="hidden" name="updated_at" value="{{ $user->updated_at }}">
</form>
更新処理の前に比較する
public function update(Request $req) {
$user = User::find($req->input('id'));
// データが更新されていれば、DBの更新日時の方が新しくなっているはず
if($user->updated_at > $req->input('updated_at')) {
// エラーメッセージを付与して入力画面へ戻す
return redirect()->back()->with('exclusive_lock_error', '排他エラーです!!'));
}
// 更新処理
$user->name = $req->input('name');
$user->email = $req->input('email');
$user->save();
// 更新が成功したら一覧画面へ
return redirect('/user/index');
}
これで排他制御は実現できますが、コントローラー汚したくないし、全ての更新処理に排他エラー時のリダイレクトを書くのはめんどくさい。。。
そこで独自例外です。
比較の処理もサービス層に任せてコントローラーをすっきりさせたいと思います。
独自例外を定義する
例外の定義自体はシンプル。これだけ
<?php
namespace App\Exceptions;
use Exception;
class ExclusiveLockException extends Exception {
// 特に何も書かなくていい
}
?>
例外発生時の処理はApp\Exceptions\Handler.phpに記述
use App\Exceptions\ExclusiveLockException;
// 例外が発生したときに呼ばれる
public function render($request, Exception $e)
{
// どの例外クラスが発生したかによって処理を分けられる。
if($e instanceof ExclusiveLockException) {
return redirect()->back()->with('exclusive_lock_exception', '排他エラーです。');
}
return parent::render($request, $e);
}
さっきの比較処理はこう修正できる。
try {
// 定義した独自例外を投げる
if($user->updated_at > $req->input('updated_at')) {
throw new ExclusiveLockException;
}
$user->name = $req->input('name');
$user->email = $req->input('email');
$user->save();
return redirect('/user/index');
} catch(ExclusiveLockException $e) {
// キャッチした例外をスローするだけ
throw $e
}
サービス層を使ってもっとすっきりさせます。
トランザクション処理も盛り込みます。
class UserService {
public function update($data) {
DB:beginTransaction();
try {
$user = User::find($data['id']);
if($user->updated_at > $data['updated_at']) {
throw new ExclusiveLockException;
}
$user->name = $data['name'];
$user->email = $data['email'];
$user->save();
DB::commit();
} catch(ExclusiveLockException $e) {
DB::rollback();
throw $e
}
}
}
protected $userService;
public function __construct(UserService $userService) {
$this->userService = $userService;
}
public function update(Request $req) {
$this->userService->update($req->all());
return redirect('/user/index')->with('successMsg', '更新しました!');
}
コントローラーから完全に排他処理を消すことができました。スッキリ◎
最後に疑問
ところで開始したトランザクションって終了しなくていいんでしょうか。
これはまあ分かるんですが
DB::transaction(function()
{
});
DB::beginTransaction()
しておいて DB::endTransaction()
的なものがないのはかなり違和感を覚えるのですが。。。