📚

MySQL トリガーでエラーを発生させる CakePHP5

2024/01/28に公開

ルールに合わないデータ更新に対してDBレベルでエラーを発生させる

データ整合性の為のルールを保ちたい場合、MySQLから見たクライアントAPPの種類が増えてくると、MySQL側で一括管理するほうが良い場合が多いと思います。その際に活躍するのがトリガーでのエラーチェック。

CREATE TRIGGER trigger_ledgers BEFORE DELETE ON ledgers
BEGIN
SELECT COUNT(*) INTO @cnt FROM ledgers
    WHERE id > OLD.id
      AND user_id = OLD.user_id;
IF @cnt > 0 THEN
    SIGNAL SQLSTATE '45000' 
      SET MESSAGE_TEXT = '過去は消せません。ただし1つ前の過去だけ消すことができます。';
END IF;
END

ledgersテーブルに対して、DELETE を許可するかどうかのトリガー。
BEFORE なので削除しようとしているレコードもまだ存在します。
積み上げ集計用のカラムがあるから、最新のレコードのみ削除可能としたい。
最新のレコードかどうかの判定を @cnt > 0 で行っています。

一般的な SQLSTATE 値を通知するには、'45000' を使用します。これは、「未処理のユーザー定義の例外」を示します。
https://dev.mysql.com/doc/refman/8.0/ja/signal.html

デメリットもある

データベースエラーなのでAPP側では致命的エラー扱い。
実装時のデバッグ環境が乏しい。
複数テーブルにかかわる場合は特にデッドロックしないようアクセスする順番を決めておこう。

CakePHP で補足できないじゃないかTT

if(this->Ledgers->delete(ledger)){...} ここで反応してほしい。
BUT データベースエラーなので致命的エラーです。「An Internal Error Has Occurred.」
ということで、トリガーは保険として、Behaviorを作ろう!

ビヘイビアでチェックしイベントチェーンを停止させる。
コードはほぼ同じ要領。

LedgersBehavior.php
public function beforeDelete(EventInterface $event, EntityInterface $entity, ArrayObject $options)
    {    
            $ledgersTable = TableRegistry::getTableLocator()->get('Ledgers');
            $ledger = $ledgersTable->find()
                ->where(['user_id'=>$entity->user_id])
                ->where(['id >'=>$entity->id])
                ->first();
            if($ledger){
                $event->stopPropagation();
                $event->setResult(false);
                return;
            }
    }

Discussion