PL/SQLの例外処理は「エラーをキャッチして何となくログに出す」程度の実装で済ませると、本番で根本原因が永遠に追えないバグを生みます。特にWHEN OTHERS THEN NULL;やWHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(SQLERRM);でエラーを握り潰すコードは、Oracle界隈で「Smothering Exception(例外窒息)」と呼ばれるアンチパターンの典型です。
多くの入門記事はBEGIN / EXCEPTION / ENDの基本構造と事前定義例外4〜5個までで止まり、実務で差が出る21種の事前定義例外全体像・PRAGMA EXCEPTION_INITでORAコードを命名・SQLCODE/SQLERRM/DBMS_UTILITY.FORMAT_ERROR_BACKTRACEによる例外追跡・例外伝播スコープ・RAISE/RAISE ex/RAISE_APPLICATION_ERRORの違い・FORALL SAVE EXCEPTIONSでのバルク処理エラー・自律トランザクションログ・SAVEPOINTとの連携までカバーした情報源がほとんどありません。
この記事ではPL/SQLの例外処理を基礎から運用レベルまで徹底解説します。例外発生の仕組み、21種の事前定義例外、ユーザー定義例外の3方式(EXCEPTION/EXCEPTION_INIT/RAISE_APPLICATION_ERROR)、エラー情報の取り方(SQLCODE/SQLERRM/FORMAT_ERROR_BACKTRACE)、Smothering Exceptionを防ぐWHEN OTHERS設計、自律トランザクションでのロバストなエラーログ、FORALL SAVE EXCEPTIONS、パッケージでの例外設計、実務パターン8選、アンチパターン8選まで2026年版の決定版です。関連する【PL/SQL】IF文完全ガイドと組み合わせて活用してください。
この記事で学べること
- 例外処理の全体像:発生→伝播→キャッチ→復旧/再RAISEのライフサイクル
- 事前定義例外21種一覧とORAコードのマッピング表
- ユーザー定義例外の3方式(
EXCEPTION宣言/PRAGMA EXCEPTION_INIT/RAISE_APPLICATION_ERROR) - エラー情報の取り方:
SQLCODE/SQLERRM/DBMS_UTILITY.FORMAT_ERROR_STACK/FORMAT_ERROR_BACKTRACE RAISE/RAISE ex/RAISE_APPLICATION_ERRORの使い分け- 例外の伝播スコープ(ネストブロック/サブプログラム/パッケージ跨ぎ)
WHEN OTHERSの正しい使い方(Smothering Exception回避)- 自律トランザクション(
PRAGMA AUTONOMOUS_TRANSACTION)でエラーログを確実に保存 SAVEPOINT+例外処理での部分ロールバック- バルク処理の
FORALL SAVE EXCEPTIONSで1件エラーで全体を止めない - パッケージでの例外カタログ設計(
-20000〜-20999の割り当て) - 実務パターン8選+アンチパターン8選
- 例外処理のライフサイクル:発生→伝播→キャッチ
- 事前定義例外21種:完全対応表
- ユーザー定義例外の3方式
- エラー情報の取得:SQLCODE/SQLERRM/BACKTRACE
- RAISE/RAISE ex/RAISE_APPLICATION_ERROR の違い
- WHEN OTHERSの正しい使い方(Smothering Exception回避)
- 自律トランザクションで確実にエラーログを保存
- SAVEPOINTで部分ロールバック
- FORALL SAVE EXCEPTIONS:バルク処理のエラー個別処理
- パッケージでの例外設計:エラー番号カタログ
- 実務パターン8選
- アンチパターン8選とリファクタリング
- よくある質問
- 関連記事
- まとめ
例外処理のライフサイクル:発生→伝播→キャッチ
例外発生
↓(同じブロックにEXCEPTION句があれば)
EXCEPTION句で WHEN 例外名 THEN をマッチング
↓(マッチしたらブロック終了、しなければ)
呼び出し元のブロックへ伝播
↓
親ブロックのEXCEPTION句でマッチング
↓(すべて不発なら)
クライアント側(SQL*Plus/JDBC/アプリ)へエラー返却
重要:マッチして処理した後は「処理済み」とみなされ、
そのブロックは正常終了扱いで親へ戻る(伝播しない)
DECLARE
v_count NUMBER;
BEGIN
SELECT COUNT(*) INTO v_count FROM users WHERE id = 9999;
IF v_count = 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'ユーザーが見つかりません');
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('データなし: ' || SQLERRM);
WHEN TOO_MANY_ROWS THEN
DBMS_OUTPUT.PUT_LINE('複数行検出: ' || SQLERRM);
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('予期せぬエラー: ' || SQLCODE || ' / ' || SQLERRM);
RAISE; -- 握りつぶさず親へ伝播
END;
/
3つの鉄則:①具体的な例外を先に、WHEN OTHERSは最後の安全網、②WHEN OTHERSでは必ずSQLCODE+SQLERRMを記録、③予期しないエラーはRAISE;で再送出して呼び出し元に伝える。
事前定義例外21種:完全対応表
PL/SQLにはOracleが組み込みで提供する例外が21種類あり、明示的な宣言なしでWHEN 例外名で受け取れます。
よく使うのはこの5つ
NO_DATA_FOUND:SELECT INTOで0件TOO_MANY_ROWS:SELECT INTOで複数件DUP_VAL_ON_INDEX:UNIQUE制約違反(INSERT/UPDATE)VALUE_ERROR:型変換・桁あふれZERO_DIVIDE:0除算
事前定義例外の使用例
DECLARE
v_name users.name%TYPE;
BEGIN
SELECT name INTO v_name FROM users WHERE id = p_id;
-- 通常処理
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20001, 'ID=' || p_id || ' のユーザーが見つかりません');
WHEN TOO_MANY_ROWS THEN
RAISE_APPLICATION_ERROR(-20002, 'ID=' || p_id || ' で複数ユーザー検出 (データ不整合)');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('予期せぬエラー: ' || SQLCODE || ' / ' || SQLERRM);
RAISE;
END;
ユーザー定義例外の3方式
方式①:EXCEPTION宣言+RAISE
DECLARE
e_invalid_amount EXCEPTION;
v_amount NUMBER := -100;
BEGIN
IF v_amount < 0 THEN
RAISE e_invalid_amount;
END IF;
EXCEPTION
WHEN e_invalid_amount THEN
DBMS_OUTPUT.PUT_LINE('金額がマイナス');
END;
方式②:PRAGMA EXCEPTION_INIT(ORAコードに名前付け)
-- 特定のORAエラーを例外名で扱えるようにする
DECLARE
-- ORA-02292(外部キー参照整合性違反)に名前付け
e_child_record_found EXCEPTION;
PRAGMA EXCEPTION_INIT(e_child_record_found, -2292);
-- ORA-02291(親キー不在)
e_parent_not_found EXCEPTION;
PRAGMA EXCEPTION_INIT(e_parent_not_found, -2291);
-- ORA-00054(リソースビジー)
e_resource_busy EXCEPTION;
PRAGMA EXCEPTION_INIT(e_resource_busy, -54);
BEGIN
DELETE FROM departments WHERE id = p_dept_id;
EXCEPTION
WHEN e_child_record_found THEN
RAISE_APPLICATION_ERROR(-20101, '子レコードが存在するため削除できません');
WHEN e_resource_busy THEN
RAISE_APPLICATION_ERROR(-20102, 'ロック待ちでタイムアウト、少し待って再試行してください');
END;
EXCEPTION_INITの真価:ORAコードをそのままWHEN OTHERS THEN IF SQLCODE = -2292 THEN ...と分岐させるより、例外名として明示するほうが可読性が格段に上。過去のORAコードを名前付きで扱えるためパッケージ・トリガーの保守性が向上します。
方式③:RAISE_APPLICATION_ERROR(呼び出し元へエラー通知)
BEGIN
-- ビジネスルール違反を例外として通知
IF :new.salary < 0 THEN
RAISE_APPLICATION_ERROR(-20001, '給与はマイナスにできません');
END IF;
-- エラー番号は -20000〜-20999 の範囲(ユーザー定義専用)
-- メッセージは2048バイトまで
-- 第3引数TRUEでエラースタック保持
IF :new.department_id NOT IN (10, 20, 30) THEN
RAISE_APPLICATION_ERROR(
-20002,
'無効な部門ID: ' || :new.department_id,
TRUE -- エラースタックに積む
);
END IF;
END;
3方式の使い分け
エラー情報の取得:SQLCODE/SQLERRM/BACKTRACE
EXCEPTION
WHEN OTHERS THEN
-- ORAエラー番号(負数)、ユーザー定義は-20000〜-20999
DBMS_OUTPUT.PUT_LINE('SQLCODE: ' || SQLCODE);
-- エラーメッセージ
DBMS_OUTPUT.PUT_LINE('SQLERRM: ' || SQLERRM);
-- エラースタック(複数エラーを全て含む)
DBMS_OUTPUT.PUT_LINE('STACK:' || CHR(10) || DBMS_UTILITY.FORMAT_ERROR_STACK);
-- エラー発生行まで辿れるトレースバック(重要!)
DBMS_OUTPUT.PUT_LINE('TRACE:' || CHR(10) || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
-- 呼び出しスタック(12cR1以降)
DBMS_OUTPUT.PUT_LINE('CALL:' || CHR(10) || DBMS_UTILITY.FORMAT_CALL_STACK);
END;
出力例:BACKTRACEで行番号まで特定
-- コード
CREATE OR REPLACE PROCEDURE proc_a IS
BEGIN
proc_b(); -- Line 3
END;
/
CREATE OR REPLACE PROCEDURE proc_b IS
v NUMBER;
BEGIN
v := 1/0; -- Line 4: ZERO_DIVIDE
END;
/
-- 呼び出し側
BEGIN
proc_a();
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE(DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
END;
-- 出力
-- ORA-06512: at "SCHEMA.PROC_B", line 4
-- ORA-06512: at "SCHEMA.PROC_A", line 3
-- ORA-06512: at line 2
-- ↑どの行でエラーが起きたか完全に追跡できる
BACKTRACEを取るタイミング:DBMS_UTILITY.FORMAT_ERROR_BACKTRACEは最後にRAISEされた地点のバックトレースを返します。ネストしたブロックで再RAISEすると上書きされるため、最初のキャッチ地点で必ず取得してログに残すのが鉄則。取り損ねると本番で原因追跡不能に陥ります。
RAISE/RAISE ex/RAISE_APPLICATION_ERROR の違い
BEGIN
-- 重い処理
process_data();
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- 既知の例外はビジネスロジックで処理
log_and_continue();
WHEN OTHERS THEN
-- 予期しないエラーはログを残してから再送出
INSERT INTO error_log(error_time, error_code, error_msg, error_backtrace)
VALUES (SYSDATE, SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
COMMIT;
RAISE; -- 重要:握り潰さない
END;
RAISE(裸)とRAISE exceptionの違い:RAISE;(裸)は現在キャッチ中の例外をそのまま再送出し、BACKTRACE/SQLCODE/SQLERRMがすべて保持されます。対してRAISE e_custom;は新しい例外に置き換わるため元の発生地点の情報が失われます(デバッグ困難に)。予期せぬエラーの再送出は必ずRAISE;(裸)で。
WHEN OTHERSの正しい使い方(Smothering Exception回避)
WHEN OTHERSは強力な「最後の安全網」ですが、間違って使うとすべての原因不明エラーを隠蔽して本番で深刻な問題を引き起こします。
❌ やってはいけないパターン(Smothering Exception)
-- ❌ 最悪:何もしない
EXCEPTION
WHEN OTHERS THEN
NULL; -- 例外窒息(Smothering)
END;
-- ❌ ログを出すだけで握り潰す
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('エラー: ' || SQLERRM);
-- ↑ DBMS_OUTPUTは本番では消えることが多い
-- 呼び出し元には何も伝わらず、正常終了として処理される
END;
-- ❌ 部分的な情報しか残さない
EXCEPTION
WHEN OTHERS THEN
INSERT INTO error_log(msg) VALUES (SQLERRM);
-- ↑ SQLCODE/BACKTRACE無し → どの行でエラーか不明
END;
⭕ 正しいパターン(完全な情報保存+再送出)
EXCEPTION
-- 既知の例外は個別処理
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20001, 'データが存在しません');
WHEN DUP_VAL_ON_INDEX THEN
RAISE_APPLICATION_ERROR(-20002, '既に登録済みです');
-- 未知の例外は完全にログして再送出
WHEN OTHERS THEN
pkg_error_log.log_error(
p_code => SQLCODE,
p_message => SQLERRM,
p_backtrace => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE,
p_context => 'proc_xxx(id=' || p_id || ')'
);
RAISE; -- ← 最重要:呼び出し元へ伝える
END;
WHEN OTHERS の3つの絶対ルール:①RAISE;またはRAISE_APPLICATION_ERRORで例外を外に出す、②SQLCODE+SQLERRM+FORMAT_ERROR_BACKTRACEを全て記録、③具体例外を先にWHENで捕まえ、OTHERSは最後の網にする。この3つが守られないPL/SQLは本番で障害が起きた時に原因が一生分からないコードになります。
自律トランザクションで確実にエラーログを保存
例外発生後にROLLBACKすると、エラーログ自体もロールバックされて失われる問題を、PRAGMA AUTONOMOUS_TRANSACTIONで解決します。
CREATE OR REPLACE PROCEDURE log_error(
p_code IN NUMBER,
p_message IN VARCHAR2,
p_backtrace IN VARCHAR2,
p_context IN VARCHAR2 DEFAULT NULL
) IS
PRAGMA AUTONOMOUS_TRANSACTION; -- 重要:独立したトランザクション
BEGIN
INSERT INTO error_log(
error_time, error_code, error_msg,
error_backtrace, context, username, client_info
) VALUES (
SYSTIMESTAMP, p_code, p_message,
p_backtrace, p_context, USER, SYS_CONTEXT('USERENV', 'CLIENT_INFO')
);
COMMIT; -- 独立COMMITで確定(呼び出し元がROLLBACKしても残る)
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
-- ログ保存すら失敗する最悪ケース(DB障害等)
-- ここでは握り潰さず呼び出し元に伝える
RAISE;
END;
/
BEGIN
UPDATE orders SET status = 'processed' WHERE id = p_id;
process_payment(p_id);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
log_error(
p_code => SQLCODE,
p_message => SQLERRM,
p_backtrace => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE,
p_context => 'process_order(' || p_id || ')'
);
ROLLBACK; -- ビジネスデータはロールバック
-- ↑ ログは自律トランザクションで既にCOMMIT済みなので残る
RAISE;
END;
エラーログテーブル設計の推奨スキーマ:error_time TIMESTAMP/error_code NUMBER/error_msg VARCHAR2(4000)/error_backtrace CLOB/context VARCHAR2(1000)/username/session_id/client_info。パーティション(月単位)+30〜90日で自動削除のライフサイクル管理が実用的。
SAVEPOINTで部分ロールバック
例外発生時にすべてをロールバックするのは過剰なケースでは、SAVEPOINTで途中地点に戻せます。大量データの一括処理で「失敗レコードだけスキップ」する時などに有用。
DECLARE
v_success_count NUMBER := 0;
v_error_count NUMBER := 0;
BEGIN
FOR rec IN (SELECT id, amount FROM orders_queue) LOOP
SAVEPOINT sp_order;
BEGIN
UPDATE accounts SET balance = balance - rec.amount WHERE id = rec.account_id;
INSERT INTO transactions(order_id, amount) VALUES (rec.id, rec.amount);
v_success_count := v_success_count + 1;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO sp_order; -- このレコードだけロールバック
v_error_count := v_error_count + 1;
log_error(
p_code => SQLCODE,
p_message => SQLERRM,
p_context => 'order_id=' || rec.id
);
END;
END LOOP;
COMMIT; -- 成功分だけ確定
DBMS_OUTPUT.PUT_LINE('成功: ' || v_success_count || ', 失敗: ' || v_error_count);
END;
SAVEPOINT使い所
SAVEPOINTはバッチ処理の部分エラー許容が典型用途。さらに詳しい使い方は【PL/SQL】SAVEPOINTを使った部分ロールバックの実装方法参照。1万件のINSERTで10件失敗しても9990件は保存される、等の堅牢な設計が可能に。
FORALL SAVE EXCEPTIONS:バルク処理のエラー個別処理
FORALLは1万件を1回のRDBMSラウンドトリップで処理する高速機能ですが、1件でもエラーが出ると全体停止するのが難点。SAVE EXCEPTIONS付きにすると「失敗した行だけスキップして処理続行」が可能です。
DECLARE
TYPE t_amounts IS TABLE OF NUMBER;
v_ids t_amounts := t_amounts(1, 2, 3, 4, 5);
v_amounts t_amounts := t_amounts(100, -50, 200, 300, NULL);
-- ↑負数 ↑NULL
e_bulk_errors EXCEPTION;
PRAGMA EXCEPTION_INIT(e_bulk_errors, -24381);
BEGIN
FORALL i IN 1..v_ids.COUNT SAVE EXCEPTIONS
UPDATE accounts SET balance = balance + v_amounts(i)
WHERE id = v_ids(i);
DBMS_OUTPUT.PUT_LINE('全件成功');
EXCEPTION
WHEN e_bulk_errors THEN
-- SQL%BULK_EXCEPTIONSで失敗レコードを個別処理
DBMS_OUTPUT.PUT_LINE('エラー件数: ' || SQL%BULK_EXCEPTIONS.COUNT);
FOR i IN 1..SQL%BULK_EXCEPTIONS.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(
'iteration ' || SQL%BULK_EXCEPTIONS(i).ERROR_INDEX ||
' → ORA-' || LPAD(SQL%BULK_EXCEPTIONS(i).ERROR_CODE, 5, '0') ||
': ' || SQLERRM(-SQL%BULK_EXCEPTIONS(i).ERROR_CODE)
);
END LOOP;
COMMIT; -- 成功分だけ確定
END;
バルク処理の詳細
バルク処理(BULK COLLECT/FORALL)の使い方は【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方、エラー処理特化は【PL/SQL】FORALLとSAVE EXCEPTIONSでバルクDMLのエラーを個別処理する方法を参照。
パッケージでの例外設計:エラー番号カタログ
プロジェクト全体で-20000〜-20999の1000枠をどう割り当てるか、事前に例外カタログを整備すると保守性が劇的に向上します。
CREATE OR REPLACE PACKAGE pkg_errors IS
-- ユーザー/認証系 -20001〜-20099
ERR_USER_NOT_FOUND CONSTANT NUMBER := -20001;
ERR_DUPLICATE_EMAIL CONSTANT NUMBER := -20002;
ERR_INVALID_PASSWORD CONSTANT NUMBER := -20003;
-- 注文/決済系 -20100〜-20199
ERR_ORDER_NOT_FOUND CONSTANT NUMBER := -20100;
ERR_INSUFFICIENT_BALANCE CONSTANT NUMBER := -20101;
ERR_INVALID_AMOUNT CONSTANT NUMBER := -20102;
-- バリデーション系 -20200〜-20299
ERR_REQUIRED_FIELD CONSTANT NUMBER := -20200;
ERR_INVALID_FORMAT CONSTANT NUMBER := -20201;
-- ヘルパー
PROCEDURE raise_error(
p_code IN NUMBER,
p_msg IN VARCHAR2
);
END pkg_errors;
/
CREATE OR REPLACE PACKAGE BODY pkg_errors IS
PROCEDURE raise_error(
p_code IN NUMBER,
p_msg IN VARCHAR2
) IS
BEGIN
RAISE_APPLICATION_ERROR(p_code, p_msg, TRUE);
END;
END pkg_errors;
/
PROCEDURE create_user(p_email VARCHAR2, p_password VARCHAR2) IS
v_count NUMBER;
BEGIN
IF p_email IS NULL THEN
pkg_errors.raise_error(pkg_errors.ERR_REQUIRED_FIELD, 'メールアドレスは必須');
END IF;
IF LENGTH(p_password) < 8 THEN
pkg_errors.raise_error(pkg_errors.ERR_INVALID_PASSWORD, 'パスワードは8文字以上');
END IF;
SELECT COUNT(*) INTO v_count FROM users WHERE email = p_email;
IF v_count > 0 THEN
pkg_errors.raise_error(pkg_errors.ERR_DUPLICATE_EMAIL, '既に登録済み: ' || p_email);
END IF;
INSERT INTO users(email, password_hash) VALUES (p_email, hash_pw(p_password));
END;
エラー番号カタログの効果:①定数名で意図が明確(ERR_DUPLICATE_EMAILは誰が見ても分かる)、②プロジェクトドキュメントとしても使える、③アプリ側(Java/Python)で同じ定数を共有するとエラーコード管理が統一、④-20000〜-20099を機能グループごとに100ずつ割り当てると拡張しやすい。
実務パターン8選
①セーフ検索(NO_DATA_FOUND対応)
FUNCTION get_user_name(p_id NUMBER) RETURN VARCHAR2 IS
v_name users.name%TYPE;
BEGIN
SELECT name INTO v_name FROM users WHERE id = p_id;
RETURN v_name;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN NULL; -- 0件ならNULLを返す仕様
-- TOO_MANY_ROWSは発生させない(主キー検索だから)
END;
②冪等なUPSERT(DUP_VAL_ON_INDEX対応)
PROCEDURE upsert_user(p_email VARCHAR2, p_name VARCHAR2) IS
BEGIN
INSERT INTO users(email, name) VALUES (p_email, p_name);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
-- 既存なら更新
UPDATE users SET name = p_name WHERE email = p_email;
END;
-- MERGE文使う方が性能は良いが、単発INSERT/UPDATEならこのパターンもOK
③処理の中断&ロールバック
BEGIN
UPDATE orders SET status = 'processing' WHERE id = p_id;
call_external_api(p_id);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
log_error(SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE, 'process_order');
ROLLBACK; -- 不整合を残さない
RAISE; -- 呼び出し元(アプリ)に通知
END;
④リソース確実クローズ
DECLARE
v_cursor SYS_REFCURSOR;
BEGIN
OPEN v_cursor FOR 'SELECT ... ';
-- 処理
CLOSE v_cursor;
EXCEPTION
WHEN OTHERS THEN
IF v_cursor%ISOPEN THEN
CLOSE v_cursor;
END IF;
RAISE;
END;
⑤入力バリデーション一括
DECLARE
v_errors VARCHAR2(4000);
BEGIN
IF p_email IS NULL THEN v_errors := v_errors || 'メール必須; '; END IF;
IF p_age IS NULL OR p_age < 18 THEN v_errors := v_errors || '年齢要件NG; '; END IF;
IF LENGTH(p_password) < 8 THEN v_errors := v_errors || 'パスワード桁数NG; '; END IF;
IF v_errors IS NOT NULL THEN
RAISE_APPLICATION_ERROR(-20200, '入力エラー: ' || v_errors);
END IF;
INSERT INTO users VALUES (p_email, p_age, hash_pw(p_password));
END;
⑥トリガー内の条件的エラー
CREATE OR REPLACE TRIGGER trg_validate_order
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
BEGIN
IF :NEW.amount IS NULL OR :NEW.amount <= 0 THEN
RAISE_APPLICATION_ERROR(-20102, '注文金額は正の数');
END IF;
IF :NEW.status NOT IN ('pending', 'paid', 'shipped', 'delivered') THEN
RAISE_APPLICATION_ERROR(-20103, '無効なステータス: ' || :NEW.status);
END IF;
END;
⑦外部キーエラーをユーザー向けに翻訳
DECLARE
e_parent_not_found EXCEPTION;
PRAGMA EXCEPTION_INIT(e_parent_not_found, -2291);
e_child_found EXCEPTION;
PRAGMA EXCEPTION_INIT(e_child_found, -2292);
BEGIN
INSERT INTO orders(user_id, amount) VALUES (p_user_id, p_amount);
EXCEPTION
WHEN e_parent_not_found THEN
RAISE_APPLICATION_ERROR(-20300, 'ユーザーID=' || p_user_id || ' が存在しません');
END;
⑧バッチの部分成功(SAVEPOINTパターン)
FOR rec IN (SELECT id, data FROM import_queue) LOOP
SAVEPOINT sp_item;
BEGIN
process_item(rec.id, rec.data);
EXCEPTION
WHEN OTHERS THEN
ROLLBACK TO sp_item;
log_error(SQLCODE, SQLERRM, DBMS_UTILITY.FORMAT_ERROR_BACKTRACE,
'import_id=' || rec.id);
END;
END LOOP;
COMMIT;
アンチパターン8選とリファクタリング
①WHEN OTHERS THEN NULL;。Oracleコミュニティで最悪のアンチパターン。例外を完全に握り潰し、本番で何が起きても気づけない。必ずRAISE;かRAISE_APPLICATION_ERRORで通知。
②WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(SQLERRM);。本番環境ではDBMS_OUTPUTバッファが消費されず/読まれないため実質握り潰し。永続テーブルへのINSERT+RAISEを組み合わせる。
③RAISE e_customで元の例外を置き換える。BACKTRACEが失われて原因追跡不能。RAISE;(裸)で情報保持。
④SQLERRMだけログに記録。SQLCODEとBACKTRACEがないと行番号・呼び出し元が分からない。必ずDBMS_UTILITY.FORMAT_ERROR_BACKTRACEを併記。
⑤例外ハンドラ内でCOMMIT。ビジネスロジック中のEXCEPTIONでCOMMITすると「中途半端な状態」で確定される。ログ保存は自律トランザクション(PRAGMA AUTONOMOUS_TRANSACTION)で分離。
⑥事前定義例外を独自例外と同じ名前にする。NO_DATA_FOUND EXCEPTION;のように再宣言するとOracle既定の例外が隠蔽され、SELECT INTOの0件が握り潰される。独自例外はe_プレフィックス等で区別。
⑦WHEN OTHERSを最初に書く。WHEN句は上から評価されるため、具体的な例外が来てもOTHERSに吸われる。具体例外→一般例外→OTHERSの順に書くのが鉄則。
⑧エラーメッセージに機密情報を載せる。RAISE_APPLICATION_ERROR(-20001, 'SELECT ... WHERE password=' || :pwd)等はアプリのエラーログに流出。メッセージは外向け情報のみ、詳細は内部ログに記録。
よくある質問
RAISE;かRAISE_APPLICATION_ERRORで再送出すること。「未知の例外を記録しつつ握り潰さない最後の安全網」として機能します。WHEN OTHERS THEN NULL;は最悪のアンチパターン。ROLLBACK;を書くか、呼び出し元のアプリケーション層/EXIT時に自動ロールバックに委ねます。例外ハンドラ内で必ずROLLBACKするかは設計方針次第。SQLERRMはエラーメッセージのみ(例:ORA-01476: divisor is zero)、DBMS_UTILITY.FORMAT_ERROR_BACKTRACEは発生した行番号と呼び出し階層を含む詳細スタックトレース。本番デバッグには必ずBACKTRACEも記録すること。RAISE;(裸)は現在キャッチ中の例外をそのまま再送出し、SQLCODE/SQLERRM/BACKTRACEが保持されます。RAISE e_custom;は新しい例外として発生し元の情報は失われます。予期せぬエラーの再送出は必ずRAISE;で。-20000〜-20999の1000枠がユーザー定義専用。これ以外はRAISE_APPLICATION_ERRORの第1引数で使うとエラー。プロジェクトで機能ごとに範囲を決め(例:ユーザー系-20000〜、注文系-20100〜)、カタログパッケージで定数化するのが実用的。STANDARDパッケージ。本記事「事前定義例外21種」の表で主要なものをすべて網羅しています。CASE文(Statement)はELSE省略でどのWHENにも該当しないとCASE_NOT_FOUND(ORA-06592)例外発生。解決は必ずELSE NULL;などを書くこと。SQLのCASE式はELSE省略時NULLを返す別挙動。IF文/CASE文の詳細は【PL/SQL】IF文完全ガイド参照。RAISE_APPLICATION_ERROR(-20xxx, 'メッセージ')を使います。トリガー内で通常の例外をRAISEすると呼び出し元のDMLが失敗し、呼び出し元アプリにエラーコード/メッセージが返ります。アプリケーション層でエラーメッセージを表示する用途に最適です。COMMIT/ROLLBACKと独立したトランザクションを開始できます。代表的な使い方は「ビジネスロジックはROLLBACKしてもエラーログは残す」。詳細は【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法参照。FORALL ... SAVE EXCEPTIONSを使うと失敗行だけスキップして処理続行できます。エラー詳細はSQL%BULK_EXCEPTIONSで個別取得。詳しくは【PL/SQL】FORALLとSAVE EXCEPTIONSでバルクDMLのエラーを個別処理する方法参照。関連記事
- 【PL/SQL】IF文完全ガイド — IFで検出→RAISEの連携パターン
- 【PL/SQL】ループ処理を行う方法 — LOOP内の例外処理
- 【PL/SQL】カーソルでSQLクエリで取得した複数の行を1行ずつ処理する方法 — Cursorと例外
- 【PL/SQL】変数・定数の使い方 — EXCEPTION型の宣言
- 【PL/SQL】初心者でもわかる基本構文とブロック構造の書き方 — PL/SQLブロック構造の土台
- 【PL/SQL】バルク処理で高速化!FORALLとBULK COLLECTの使い方 — バルク処理の基礎
- 【PL/SQL】FORALLとSAVE EXCEPTIONSでバルクDMLのエラーを個別処理する方法 — バルクエラー詳解
- 【PL/SQL】AUTONOMOUS TRANSACTIONで独立した処理を行う方法 — エラーログの分離
- 【PL/SQL】SAVEPOINTを使った部分ロールバックの実装方法 — 部分復帰の設計
- 【PL/SQL】トリガーを作成する方法と具体例 — トリガー内の例外
- 【PL/SQL】ストアドプロシージャとファンクションの違いと作り方 — プロシージャでの例外設計
まとめ
- 例外ライフサイクル:発生→同ブロックのEXCEPTIONでマッチ→未マッチなら親へ伝播
- 事前定義例外21種:NO_DATA_FOUND/TOO_MANY_ROWS/DUP_VAL_ON_INDEX/VALUE_ERROR/ZERO_DIVIDEが頻出
- ユーザー定義3方式:EXCEPTION宣言/PRAGMA EXCEPTION_INIT/RAISE_APPLICATION_ERROR
- エラー情報4種:
SQLCODE/SQLERRM/FORMAT_ERROR_STACK/FORMAT_ERROR_BACKTRACE(行番号必須) RAISE;(裸)は情報保持再送出、RAISE eは情報失う→裸推奨RAISE_APPLICATION_ERROR(-20000〜-20999, msg)でアプリ層へメッセージ通知WHEN OTHERSは最後の網+ログ+RAISE;が3原則(Smothering回避)- PRAGMA AUTONOMOUS_TRANSACTIONでログ保存を独立COMMIT(ROLLBACKで消えない)
SAVEPOINT+ROLLBACK TOで部分復帰が可能FORALL SAVE EXCEPTIONSでバルク処理の失敗行だけスキップ- エラー番号カタログパッケージで-20000〜-20999の1000枠を機能別に割当
- アンチパターン:WHEN OTHERS THEN NULL/DBMS_OUTPUTだけ/RAISE eで置換/SQLERRMのみログ/ハンドラ内COMMIT/事前定義名の再宣言/OTHERSを最初/機密情報メッセージ
PL/SQLの例外処理は「エラーを受ける」だけでなく、発生地点を完全に記録し、適切な粒度で呼び出し元へ通知することまで含めた設計が本質です。本記事の21種事前定義例外リファレンス、3方式のユーザー定義、自律トランザクション+SAVEPOINT+FORALL SAVE EXCEPTIONSを組み合わせれば、本番で原因追跡が困難なコードからいつでもデバッグできる堅牢なコードへと進化できます。IF文はIF文完全ガイド、バルク処理はFORALL/BULK COLLECT、自律トランザクションはAUTONOMOUS TRANSACTION、SAVEPOINTはSAVEPOINT部分ロールバックと合わせてご活用ください。
