laravelで更新時ロックをかける際、レコードロックとテーブルロックでは書き方が異なる・両者を同時に使えない。
動作検証 : laravel 10 / mysql
トランザクション
レコード更新時にロックをかけ、更新する。トランザクション内で実行する。
↓DBファザードのtransactionメソッドを利用。コミットやロールバックが自動で行われる。
$params = ['studentId' => 'student001', 'studentName' => 'てすと花子']; DB::transaction(function () use ($params) { // 指定の生徒を取得・ロックする★ // 該当の生徒の選択講座のテーブルをロックする★★ // レコード更新する }, 3);
対象レコードの専有ロック ★
- ある生徒レコードに対しての専有ロック。トランザクションが終わるまで参照更新不可。
↓ Model(クエリビルダ)利用の場合。OK、ロックかかる
DB::transaction(function () use ($params) { // 指定の生徒を取得・ロックする $student = Student::where('student_id', $params['studentId'])->lockForUpdate()->first(); // 指定の生徒を更新 $student->student_name = $params['studentName']; $student->save(); }, 3);
↓ DBクラス(SQLクエリ)利用の場合。OK、ロックかかる
DB::transaction(function () use ($params) { // 指定の生徒を取得・ロックする $sql = "SELECT * FROM students WHERE student_id = :studentId FOR UPDATE"; $result = DB::select($sql, ['studentId' => $params['studentId']]); // 指定の生徒を更新 $sqlu = "UPDATE students SET student_name = :studentName WHERE student_id = :studentId"; $resultu = DB::update($sqlu, $params); }, 3);
テーブルロック ★★
- ある生徒の、選択講座複数レコードの追加・更新・削除処理。
講座の予約が満席か等の参照も、件数が変動するため処理が終わるまでは不可。テーブルロックとする。 - Eloquentのクエリビルダを使う方法はなさそう。生クエリ実行する。
(参考)
https://qiita.com/zackey2/items/079128df6567091b8a1c
↓ OK、テーブルロックかかる
DB::transaction(function () use ($params) { // 自動コミットをOFFにする DB::unprepared('SET AUTOCOMMIT = 0'); // 選択講座テーブルをロックする DB::unprepared('LOCK TABLES selected_lectures WRITE'); // 選択講座の追加・更新・削除 //(略) }, 3);
「対象レコードの専有ロック+テーブルロック」は エラーになる
上記2つ、対象生徒のレコードと、その生徒の選択講座テーブル を同時にロックする。
単純に2つを併記すると・・NG、エラーになった。
$param = []; DB::transaction(function () use ($params) { DB::unprepared('SET AUTOCOMMIT = 0'); // 選択講座テーブルをロックする DB::unprepared('LOCK TABLES selected_lectures WRITE'); // 指定の生徒を取得・ロックする★ $sql = "SELECT * FROM students WHERE student_id = :studentId FOR UPDATE"; $result = DB::select($sql, ['studentId' => $params['studentId']]); // 指定の生徒を更新 $sqlu = "UPDATE students SET student_name = :studentName WHERE student_id = :studentId"; $resultu = DB::update($sqlu, $params); // 選択講座の追加・更新・削除 //(略) }, 3);
DB::update()で、studentsがロックされてないと言われる。★が効いていない模様SQLSTATE[HY000]: General error: 1100 Table 'students' was not locked with LOCK TABLES
複数テーブルをロック:OK
studentsも、テーブルロック指定するとOKだった。
ロック範囲が大きくなるのでstudentsは特定のレコードのみのロックでよいのだが、できないっぽい。
↓ OK、テーブルロックかかる
DB::transaction(function () use ($params) { DB::unprepared('SET AUTOCOMMIT = 0'); // 選択講座テーブル、生徒テーブルもロックする DB::unprepared('LOCK TABLES selected_lectures WRITE, students WRITE'); // FOR UPDATEなしで取得 $sql = "SELECT * FROM students WHERE student_id = :studentId"; $result = DB::select($sql, ['studentId' => $params['studentId']]); // 指定の生徒を更新 $sqlu = "UPDATE students SET student_name = :studentName WHERE student_id = :studentId"; $resultu = DB::update($sqlu, $params); // 選択講座の追加・更新・削除 //(略) }, 3);
おまけ:自動コミット指定なしのエラー
DB::unprepared('SET AUTOCOMMIT = 0');
がないと、There is no active transaction
のエラーになる。
DB::update()時点で自動コミット・トランザクションが終了し、DB::transaction()抜ける時にトランザクションがない!ということみたい。
おまけ:ロック中、modelからリンクテーブルを参照するとエラー
予約システムの実コード、モデルを利用した場合にエラーが出た。
public static function findBystudentId(studentId$studentId, bool $lockForUpdate = false) : ?StudentEntity { if ($lockForUpdate === true) { DB::unprepared('SET AUTOCOMMIT = 0'); //生徒テーブルと、選択講座テーブルもロックする DB::unprepared('LOCK TABLES students WRITE, selected_lectures WRITE'); } $student = Student::where('student_id', $studentId->toString())->first(); if (!$student) { return $student; } //(略) return $student->toEntity(); //★★★ }
General error: 1100 Table 'lectures' was not locked with LOCK TABLES at /var/www/html/vendor/laravel/framework/src/Illuminate/Database/Connection.php:423)
最後の★★★内の処理で、呼んでいるmodelに講座テーブル(lectures)へのリンクがあったのが原因。
参照しているだけなのに・・・
(リンクしていると保存も可能なのか?だからロックを要求されている?? 未調査。)
解決方法:LOCK TABLES に 「lectures READ」を追加した。無用なロックが増えた.
modelのリンクなどlaravelは自動で便利な機能も多いけど、条件が重なると不都合が出てくる。仕方ないと言えば仕方ない。
※もしかしたらリンクを無効にする方法があるのかもしれない。未調査
DB::unprepared('LOCK TABLES students WRITE, selected_lectures WRITE, lectures READ');
model:リンクを参照している部分
namespace App\Models; class SelectedLecture extends Model // リンク public function lecture() { return $this->belongsTo(Lecture::class, 'lecture_id', 'lecture_id'); } public function toEntity(): SelectedLectureEntity { //略 if ($this->lecture) { //ココ!!!!! $selectedLectureEntity->setLecture($this->lecture->toEntity()); } return $selectedLectureEntity; }