MySQL 9.7では、長らく要望されていた改善が導入されました。SQLレイヤーで外部キーのカスケード処理が実行される際、子テーブルのトリガーも実行されるようになりました。従来、InnoDB内部で実行されるカスケード処理では、子テーブルのトリガーは実行されませんでした。そのため、監査、派生データのメンテナンス、オブザーバビリティなどにおいて、変更を捕捉できないケースがありました。

親テーブルの行を変更した結果、子テーブルでもカスケードによる変更が発生した場合、それらの子テーブルのトリガーは実行されませんでした。この挙動は、監査やビジネスルールの適用にトリガーを利用している開発者にとって、しばしば想定外のものでした。今回の変更により、正確性オブザーバビリティが大きく改善されます。また、明示的なオプトイン方式を採用することで、後方互換性も維持されます。

問題:カスケードは発生するのに、トリガーは何も反応しない

MySQLでは、親テーブルの行が更新または削除されると、外部キー制約によって子テーブルにもカスケード変更が発生することがあります。たとえば、ON DELETE CASCADE では依存する行が削除され、ON UPDATE CASCADE では外部キー参照が自動的に更新されます。

しかし従来、このようなカスケードによる変更では、子テーブルのトリガーは実行されませんでした。これはMySQLに長く存在していた制限であり、MySQL Bug #11472にも、カスケード処理では影響を受ける子テーブルのトリガーが呼び出されないことが記録されています。

その結果、監査やビジネスロジックをトリガーに依存しているシステムでは、外部キーのカスケードによって発生した重要な変更を、気付かないまま取りこぼす可能性がありました。データの観点では変更は正しく行われています。しかし、トリガーの観点から見ると、その変更は存在しないのと同じでした。

これにより、実環境では次のような問題が発生していました。

  • 子テーブルの監査トリガーが、カスケードによる変更を検知できない
  • トリガーによって管理しているマテリアライズド列や非正規化列が古い状態のままになる
  • トリガーに実装したビジネスルールが実行されない
  • デバッグやオブザーバビリティが難しくなるる

開発者は一般に、行が変更されたのであれば、その変更がSQL文から直接発生したのか、外部キーのカスケードによって発生したのかにかかわらず、トリガーが実行されることを期待します。この「期待される挙動」と「実際の挙動」の違いが、本番環境で意図しない結果につながることもありました。

例:
子テーブルにトリガーを設定した、単純な親子関係を考えてみます。

CREATE TABLE child (cid INT PRIMARY KEY, parent_id INT, 
FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE)

CREATE TABLE audit_log (message VARCHAR(255))

INSERT INTO parent VALUES (1) INSERT INTO child VALUES (1,1)

CREATE TRIGGER child_delete_trigger AFTER DELETE ON child 
FOR EACH ROW INSERT INTO audit_log VALUES ('Child row deleted')

MySQL 9.7より前の挙動:

親テーブルの行を削除します。

DELETE FROM parent WHERE id = 1

MySQL 9.7より前では、カスケードによって子行は削除されますが、トリガーは実行されません。その結果、監査テーブルは空のままです。この動作は外部キーの観点では技術的に正しいものの、トリガーの観点では予想外です。

MySQL 9.7での挙動:

MySQL 9.7では、SQLレイヤーで外部キーのカスケード処理中にトリガーを実行できるようになりました。新しいMySQLサーバー変数 enable_cascade_triggers を設定することで有効化できます。

この機能を有効にします。

SET enable_cascade_triggers = ON

同じDELETE文を実行します。

DELETE FROM parent WHERE id = 1

すると、次のような動作になります。

  • 子テーブルの行が削除される
  • 子テーブルのトリガーが実行される
  • 監査レコードが挿入される

これにより、カスケードによって発生した変更もトリガーロジックから認識できるようになり、MySQLの挙動がユーザーの期待に近づきます。

解決策:変更をトリガーから認識できるようにする

従来、外部キーのカスケード処理はInnoDBのストレージエンジン内部で処理されていました。一方、トリガーはSQLレイヤーに実装されているため、ストレージエンジンレベルで実行されるカスケード処理からトリガーを呼び出すことはできませんでした。

MySQL 9.6 からは、SQLレイヤーでの外部キー処理 が導入され、サーバー起動時の変数 innodb_native_foreign_keys によって制御されています。

SQLレイヤーで外部キーを有効にした場合:innodb_native_foreign_keys = OFF(デフォルト)

  • カスケード処理はサーバーレイヤーで実行される
  • トリガーを呼び出せるようになる
  • 再帰やカスケードチェーンを追跡できる
  • 安全性のための制限を適用できる

このアーキテクチャ変更によって、カスケード処理中にトリガーを実行することが技術的に可能になりました。

カスケード中に実行される子テーブルのトリガー

カスケード中のトリガー実行を有効にすると、MySQLは子テーブルの BEFORE トリガーと AFTER トリガーの両方を実行します。動作はカスケードの種類によって異なります。

  • ON DELETE CASCADE → DELETEトリガーが実行される
  • ON UPDATE CASCADE → UPDATEトリガーが実行される
  • ON DELETE SET NULL → UPDATEトリガーが実行される
  • ON UPDATE SET NULL → UPDATEトリガーが実行される

これにより、行の変更を前提としたトリガーロジックが一貫して実行されるようになります。

トリガーの実行順序

特に親テーブルと子テーブルの両方にトリガーが設定されている場合、トリガーの実行順序は重要です。親テーブルの行を削除した結果、カスケードが発生する場合、MySQLは予測可能な順序でトリガーを実行します。

実行順序は次のとおりです:

  1. DELETE FROM parent WHERE id = 1
  2. 親テーブルのBEFORE DELETEトリガーを実行
  3. 子テーブルへカスケードする
  4. 子テーブルのBEFORE DELETEトリガーを実行
  5. 子テーブルの行を削除
  6. 子テーブルのAFTER DELETEトリガーを実行
  7. 親テーブルの行を削除
  8. 親テーブルのAFTER DELETEトリガーを実行

この決定論的な順序により、トリガーは一貫性を保ちながら、予測可能な形で動作します。複数レベルのカスケード、たとえば parent → child → grandchild のような構造では、親テーブルの行を削除すると、カスケードはチェーンに沿って深さ優先(depth-first)で伝播します。

トランザクションの挙動

カスケード中のトリガー実行は、同じSQL文および同じトランザクションの一部として行われます。カスケード処理中にいずれかのトリガーが失敗すると、文全体がロールバックされます。

たとえば、子テーブルのトリガーがエラーを発生させた場合:

  • 親テーブルの削除が中止される
  • 子テーブルへの変更がロールバックされる
  • トランザクションの整合性が維持される

これにより、ACID特性が維持され、データ整合性が確保されます。

安全性の制御と再帰制限

再帰的なカスケードや意図しない副作用を防ぐため、子テーブルに定義されたトリガーが親テーブルを変更しようとした場合や、その他の方法で同じカスケード経路に再度入ろうとした場合、サーバーがその状態を検出し、エラーを返します。

外部キーのカスケードとトリガーを組み合わせると、複雑な依存関係が形成される可能性があります。たとえば、あるトリガーが別のテーブルを変更し、その結果としてさらに別のカスケードが発生する可能性があります。カスケード処理を予測可能で管理しやすいものにするため、MySQLでは、1つのSQL文の実行中にカスケードチェーンへ参加できるテーブル数に上限を設けています。すべてのカスケード経路に関与するテーブルの合計数が最大許容値(デフォルトは30)を超えると、サーバーはそれ以上の処理を停止し、エラーを返します。

この安全策により、過度に複雑なカスケードチェーンを防ぎ、デバッグが困難になったり、リソースを過剰に消費したりすることを防ぎます。カスケードの深さがこの制限を超えると、MySQLはSQL文を中止し、トランザクションをロールバックします。

サーバーのオプトイン変数を使った安全な導入

アプリケーションによっては、カスケード時にトリガーが実行されないことを前提としている場合があります。そのため、カスケード時のトリガー実行を有効にすると、特に監査ログやビジネスルールの適用において、アプリケーションの動作が変わる可能性があります。

このため、この機能はオプトイン方式になっています。次の手順で導入することが推奨されます:

  • まずセッション単位で有効にする
  • 十分にテストする
  • 段階的に展開する

後方互換性を維持するため、カスケード時のトリガー実行はデフォルトで無効になっています。ユーザーがシステム変数 enable_cascade_triggers を明示的に有効化する必要があります。

設定変数

この機能は、次の2つの変数で制御します。

  • enable_cascade_triggers
  • innodb_native_foreign_keys

enable_cascade_triggers は動的変数であり、セッション単位で設定できます。そのため、段階的な導入やテストが可能です

子テーブルのトリガー実行の挙動:

innodb_native_foreign_keysenable_cascade_triggersChild table trigger execution
OFF(Default)OFF(Default)No
OFFONYes
ONOFFNo
ONONNo

カスケードトリガーはいつ有効にすべきか?

次のような場合は、カスケードトリガーを有効にすることを検討してください:

  • トリガーを使って監査を実装している
  • 派生テーブルがトリガーに依存している
  • ビジネスロジックが子テーブルのトリガーに依存している
  • カスケードによる変更をオブザーバビリティの対象にする必要がある

次のような場合は、有効化を避けることを推奨します:

  • 非常に大規模なカスケード操作が頻繁に発生する
  • トリガーに負荷の大きい処理が含まれている
  • 多段階のカスケードを頻繁に利用している

まとめ

外部キーは隠れた変更を作るべきではなく、その変更が発生しているにもかかわらず、トリガーが何も反応しない状態も望ましくありません。約20年にわたってMySQL Bug #11472で追跡されてきたこの問題は、MySQL 9.7でようやく、SQLレイヤーの外部キーカスケード中にトリガーを実行できるようになったことで解決されました。

この改善により、以下の点が向上します:

  • オブザーバビリティ
  • 監査可能性
  • データ整合性
  • 予測可能性

この機能をオプトイン方式とし、安全性のための制限も設けることで、MySQLは正確性と後方互換性のバランスを取っています。トリガーと外部キーを組み合わせて利用しているアプリケーションにとって、今回の機能強化はMySQLのリレーショナルデータ整合性モデルにおける重要な前進と言えるでしょう。