【PL/SQL】例外をJSON形式で返すREST対応API設計

【PL/SQL】例外をJSON形式で返すREST対応API設計 PL/SQL

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ステータスは別レイヤで扱い、ボディの構造は常に一定にしておくと、クライアント側の実装が安定します。

error-response.json
{
  "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 で埋め込みます。

api_error.pks-pkb
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_detailsp_payload に不正なJSONを渡すと、JSON生成時にエラーになります。外部入力をそのまま渡すのではなく、呼び出し側でも JSON_OBJECTJSON_ARRAYAGG で正規のJSONを作ってから渡します。

代表的な例外の分類と名前付き例外

PL/SQL内で対象となる例外を明示しておくと、ファサード層での分岐が読みやすくなります。PRAGMA EXCEPTION_INIT で主要なORAを名前付き例外にバインドし、API層でドメインコードへ変換します。

err_map.pks
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 で成形しておくと、成功時も失敗時も同じルールで扱えます。

order_domain.pks-pkb
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_INFODBMS_SESSION.SET_IDENTIFIER でセッション情報に載せ、分類に基づいてJSONを返します。ORDSで公開する場合は、このファサード関数をRESTハンドラから呼び出します。監視やトレースには DBMS_APPLICATION_INFO完全ガイド も役立ちます。

api_upsert_order.sql
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ステータスを分けて設計すると、クライアント側はステータスで大別し、coderetryable で詳細分岐できます。

ords-handler-example.sql
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 で詳細分岐します。

400/409入力不備、重複、一意制約違反など。再送前に入力やリクエストIDを見直します。
403権限不足。DB権限やAPI認可を確認します。
404対象リソースなし。ドメインコードでNOT_FOUNDなどを返します。
409ロック競合やデッドロックなど。retryable=trueなら安全な再試行を検討します。
500想定外例外。詳細は相関IDでログを追跡します。

再試行と冪等性の取り扱い

再試行可能な例外を返す以上、同一要求の重複実行で破壊的変化が生じないことが前提になります。自然キーやリクエストIDの一意制約を基盤にし、更新は MERGE や条件付き UPDATE で収束させます。外部通知はアウトボックステーブルに記録してから配送すると、API再試行と外部副作用の二重化を切り離しやすくなります。

監査・トレースとの連携

DBMS_SESSION.SET_IDENTIFIER に相関IDを入れておくと、V$SESSION や監査テーブルでリクエスト単位の追跡がしやすくなります。自治トランザクションの軽量ロガーを用意し、失敗時に相関ID、コード、入力キー、経過時間を確定させると、障害対応の初動が速くなります。自律トランザクションの注意点は PRAGMA AUTONOMOUS_TRANSACTION完全ガイド も確認してください。

api_audit.sql
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_OBJECTJSON_ARRAYAGG では RETURNING CLOB を指定します。ORDS側のメディアタイプは application/json、文字コードはUTF-8に統一します。

テストの着眼点

正常系のJSON整形と同じ重みで異常系を網羅します。一意制約違反、ロック競合、デッドロック、権限不足、想定外例外、存在しないリソースの各ケースで、HTTPステータス、ボディの coderetryablecorrelationId が期待通りであることを確認します。

  • 成功時に ok:truedata が返る
  • 失敗時に 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と監査を整えることで、障害時の切り分けと再実行が予測可能になります。