REST対応のPL/SQLでは、失敗時に例外をそのまま投げ返すのではなく、クライアントが機械的に解釈できるJSONへ正規化して返す設計が重要です。業務エラー、一時的な競合、権限不足、想定外例外を同じスキーマで包むことで、フロントエンド、バッチ、外部連携先の分岐を簡潔にできます。
この記事では、Oracle 19cでも動かしやすい書き方を前提に、JSON_OBJECT、例外マッピング、ORDS公開、HTTPステータス、相関ID、監査ログまでを実運用向けに整理します。SQL側でPL/SQLの BOOLEAN をそのまま扱えない点や、JSONの真偽値を FORMAT JSON で出す点も補足します。
- Oracle Database 19cを主な前提にする
- レスポンス本文はCLOBのJSONとして返す
- HTTPステータスはORDSなどのHTTP層で別途制御する
- エラー本文にはドメインコード、再試行可否、相関IDを含める
エラーの共通スキーマを先に決める
返却するJSONの骨格を固定します。成功時は ok: true、失敗時は ok: false を入れ、コード、メッセージ、リトライ可否、相関ID、追加メタ情報を格納できるようにします。HTTPステータスは別レイヤで扱い、ボディの構造は常に一定にしておくと、クライアント側の実装が安定します。
{
"ok": false,
"code": "DUPLICATE",
"message": "duplicate request id",
"retryable": false,
"correlationId": "7F4A9C1E...",
"details": {"key":"REQ-12345"}
}
例外コードの正規化とマッピング方針
ORAコードをそのまま外部へ出さず、業務ドメインに沿った短いコードへ写像します。ロック競合、デッドロック、シリアライズ競合のように再実行で解消する可能性があるケースは再試行可能にし、入力不備、一意制約違反、権限不足は非再試行として扱います。
デッドロックは ORA-00060の原因と解決方法、一意制約違反は ORA-00001の原因と解決方法、権限不足は ORA-01031完全ガイド と関連します。
共通フォーマッタと相関IDの実装
フォーマッタはエラーJSONを組み立てる専用ユニットとして独立させます。Oracle 19cではSQL側にPL/SQLの BOOLEAN を直接渡せないため、PL/SQL内で 'true' / 'false' のJSONリテラル文字列へ変換し、FORMAT JSON を付けてJSONの真偽値として出力します。追加情報や成功ペイロードもJSON文字列として渡し、FORMAT JSON で埋め込みます。
CREATE OR REPLACE PACKAGE api_error AS
FUNCTION corr_id RETURN VARCHAR2;
FUNCTION json_fail(
p_code VARCHAR2,
p_msg VARCHAR2,
p_retryable BOOLEAN,
p_corr VARCHAR2,
p_details CLOB DEFAULT NULL
) RETURN CLOB;
FUNCTION json_ok(
p_payload CLOB,
p_corr VARCHAR2
) RETURN CLOB;
END;
/
CREATE OR REPLACE PACKAGE BODY api_error AS
FUNCTION corr_id RETURN VARCHAR2 IS
BEGIN
RETURN RAWTOHEX(SYS_GUID());
END;
FUNCTION json_fail(
p_code VARCHAR2,
p_msg VARCHAR2,
p_retryable BOOLEAN,
p_corr VARCHAR2,
p_details CLOB
) RETURN CLOB IS
v_json CLOB;
v_retry_json VARCHAR2(5);
BEGIN
IF p_retryable THEN
v_retry_json := 'true';
ELSE
v_retry_json := 'false';
END IF;
SELECT JSON_OBJECT(
'ok' VALUE 'false' FORMAT JSON,
'code' VALUE p_code,
'message' VALUE p_msg,
'retryable' VALUE v_retry_json FORMAT JSON,
'correlationId' VALUE p_corr,
'details' VALUE COALESCE(p_details, 'null') FORMAT JSON
RETURNING CLOB)
INTO v_json
FROM dual;
RETURN v_json;
END;
FUNCTION json_ok(
p_payload CLOB,
p_corr VARCHAR2
) RETURN CLOB IS
v_json CLOB;
BEGIN
SELECT JSON_OBJECT(
'ok' VALUE 'true' FORMAT JSON,
'correlationId' VALUE p_corr,
'data' VALUE COALESCE(p_payload, 'null') FORMAT JSON
RETURNING CLOB)
INTO v_json
FROM dual;
RETURN v_json;
END;
END;
/
p_details や p_payload に不正なJSONを渡すと、JSON生成時にエラーになります。外部入力をそのまま渡すのではなく、呼び出し側でも JSON_OBJECT や JSON_ARRAYAGG で正規のJSONを作ってから渡します。
代表的な例外の分類と名前付き例外
PL/SQL内で対象となる例外を明示しておくと、ファサード層での分岐が読みやすくなります。PRAGMA EXCEPTION_INIT で主要なORAを名前付き例外にバインドし、API層でドメインコードへ変換します。
CREATE OR REPLACE PACKAGE err_map IS deadlock_ex EXCEPTION; PRAGMA EXCEPTION_INIT(deadlock_ex, -60); -- ORA-00060 lock_nowait_ex EXCEPTION; PRAGMA EXCEPTION_INIT(lock_nowait_ex, -54); -- ORA-00054 seri_conflict EXCEPTION; PRAGMA EXCEPTION_INIT(seri_conflict, -8177); -- ORA-08177 unique_violate EXCEPTION; PRAGMA EXCEPTION_INIT(unique_violate, -1); -- ORA-00001 no_priv EXCEPTION; PRAGMA EXCEPTION_INIT(no_priv, -1031); -- ORA-01031 END; /
業務処理はドメインパッケージへ、返却はファサードでJSON化
処理本体は成功か例外かだけを意識し、レスポンス形式への変換はAPIファサードに委ねます。成功時のデータは事前に JSON_OBJECT で成形しておくと、成功時も失敗時も同じルールで扱えます。
CREATE OR REPLACE PACKAGE order_domain AS
PROCEDURE upsert_order(p_req_id VARCHAR2, p_cust NUMBER, p_amt NUMBER);
FUNCTION view_order(p_req_id VARCHAR2) RETURN CLOB;
END;
/
CREATE OR REPLACE PACKAGE BODY order_domain AS
PROCEDURE upsert_order(p_req_id VARCHAR2, p_cust NUMBER, p_amt NUMBER) IS
BEGIN
MERGE INTO orders d
USING (SELECT p_req_id req_id, p_cust cust, p_amt amt FROM dual) s
ON (d.req_id = s.req_id)
WHEN MATCHED THEN
UPDATE SET d.cust_id = s.cust,
d.amount = s.amt,
d.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN
INSERT (req_id, cust_id, amount, created_at)
VALUES (s.req_id, s.cust, s.amt, SYSTIMESTAMP);
END;
FUNCTION view_order(p_req_id VARCHAR2) RETURN CLOB IS
v_json CLOB;
BEGIN
SELECT JSON_OBJECT(
'reqId' VALUE req_id,
'custId' VALUE cust_id,
'amount' VALUE amount
RETURNING CLOB)
INTO v_json
FROM orders
WHERE req_id = p_req_id;
RETURN v_json;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20010, 'order not found');
END;
END;
/
REST用ファサード関数の設計と例外マッピング
相関IDを生成し、DBMS_APPLICATION_INFO と DBMS_SESSION.SET_IDENTIFIER でセッション情報に載せ、分類に基づいてJSONを返します。ORDSで公開する場合は、このファサード関数をRESTハンドラから呼び出します。監視やトレースには DBMS_APPLICATION_INFO完全ガイド も役立ちます。
CREATE OR REPLACE FUNCTION api_upsert_order(
p_req_id VARCHAR2,
p_cust NUMBER,
p_amt NUMBER
) RETURN CLOB
IS
v_corr VARCHAR2(64) := api_error.corr_id();
BEGIN
DBMS_APPLICATION_INFO.SET_MODULE('ORDERS_API', 'UPSERT');
DBMS_SESSION.SET_IDENTIFIER(v_corr);
order_domain.upsert_order(p_req_id, p_cust, p_amt);
RETURN api_error.json_ok(
p_payload => JSON_OBJECT('reqId' VALUE p_req_id RETURNING CLOB),
p_corr => v_corr);
EXCEPTION
WHEN err_map.lock_nowait_ex OR err_map.deadlock_ex OR err_map.seri_conflict THEN
RETURN api_error.json_fail(
p_code => 'RETRYABLE_CONFLICT',
p_msg => 'concurrency conflict',
p_retryable => TRUE,
p_corr => v_corr,
p_details => JSON_OBJECT('reqId' VALUE p_req_id RETURNING CLOB));
WHEN err_map.unique_violate THEN
RETURN api_error.json_fail(
p_code => 'DUPLICATE',
p_msg => 'duplicate request id',
p_retryable => FALSE,
p_corr => v_corr,
p_details => JSON_OBJECT('reqId' VALUE p_req_id RETURNING CLOB));
WHEN err_map.no_priv THEN
RETURN api_error.json_fail('FORBIDDEN', 'insufficient privileges', FALSE, v_corr, NULL);
WHEN OTHERS THEN
RETURN api_error.json_fail('UNEXPECTED', SQLERRM, FALSE, v_corr, NULL);
END;
/
ORDSで公開する時の考え方
ORDSでは、PL/SQLブロックの戻り値をHTTPレスポンスとして返します。本文は上記の共通JSONスキーマにし、HTTPステータスやメディアタイプはORDS側のハンドラ、レスポンスヘッダ、または実装方針に合わせて制御します。本文のJSONとHTTPステータスを分けて設計すると、クライアント側はステータスで大別し、code と retryable で詳細分岐できます。
BEGIN
:body := api_upsert_order(
p_req_id => :req_id,
p_cust => :cust_id,
p_amt => :amount);
-- ORDS側では application/json として返す設計にする
-- HTTPステータスはハンドラ設計に応じて別途制御する
END;
HTTPステータスとの対応付け
再試行可能な競合は409、重複や入力不正は400または409、権限不足は403、見つからない系は404、想定外は500を割り当てると分岐が単純化します。ステータスはヘッダ、本文は共通スキーマという二層で設計し、クライアントはまずステータスで大別し、ボディの code で詳細分岐します。
再試行と冪等性の取り扱い
再試行可能な例外を返す以上、同一要求の重複実行で破壊的変化が生じないことが前提になります。自然キーやリクエストIDの一意制約を基盤にし、更新は MERGE や条件付き UPDATE で収束させます。外部通知はアウトボックステーブルに記録してから配送すると、API再試行と外部副作用の二重化を切り離しやすくなります。
監査・トレースとの連携
DBMS_SESSION.SET_IDENTIFIER に相関IDを入れておくと、V$SESSION や監査テーブルでリクエスト単位の追跡がしやすくなります。自治トランザクションの軽量ロガーを用意し、失敗時に相関ID、コード、入力キー、経過時間を確定させると、障害対応の初動が速くなります。自律トランザクションの注意点は PRAGMA AUTONOMOUS_TRANSACTION完全ガイド も確認してください。
CREATE OR REPLACE PROCEDURE api_audit( p_corr VARCHAR2, p_code VARCHAR2, p_msg VARCHAR2 ) IS PRAGMA AUTONOMOUS_TRANSACTION; BEGIN INSERT INTO api_log(correlation_id, code, message, at) VALUES (p_corr, p_code, SUBSTR(p_msg, 1, 4000), SYSTIMESTAMP); COMMIT; END; /
ペイロードサイズと型の選択
JSONの生成や返却はCLOBを基本に据えると安全です。VARCHAR2の制限を超える可能性がある応答は最初からCLOBで設計し、JSON_OBJECT や JSON_ARRAYAGG では RETURNING CLOB を指定します。ORDS側のメディアタイプは application/json、文字コードはUTF-8に統一します。
テストの着眼点
正常系のJSON整形と同じ重みで異常系を網羅します。一意制約違反、ロック競合、デッドロック、権限不足、想定外例外、存在しないリソースの各ケースで、HTTPステータス、ボディの code、retryable、correlationId が期待通りであることを確認します。
- 成功時に
ok:trueとdataが返る - 失敗時に
ok:falseとドメインコードが返る - 再試行可能な競合だけ
retryable:trueになる - 相関IDでAPIログとDBセッションを追跡できる
- 不正なdetails/payload JSONを渡した時の扱いを決めている
まとめ
REST対応のPL/SQLでは、例外をその場しのぎで返さず、短いドメインコード、再試行可否、相関IDを含む共通スキーマのJSONに正規化することが安定運用の土台になります。Oracle 19cでは、JSONの真偽値や埋め込みJSONを扱う時に FORMAT JSON を使い、RETURNING CLOB でサイズ上限を避けるのが安全です。
処理本体は成功か例外のみを意識し、ファサードでHTTPステータスとボディの二層を確定させます。冪等キーで再試行を安全にし、相関IDと監査を整えることで、障害時の切り分けと再実行が予測可能になります。

