【PL/SQL】パッケージAPI設計における互換性維持とバージョン戦略

【PL/SQL】パッケージAPI設計における互換性維持とバージョン戦略 PL/SQL

PL/SQLのパッケージ仕様部は、一度公開すると利用側のコンパイル済みコード、バッチ、外部アプリケーションから参照されます。そのため、引数を1つ変えただけでも依存オブジェクトがINVALIDになり、戻り値やコミット方針を変えるとコンパイルが通っても業務動作が壊れることがあります。

この記事では、パッケージAPIを長期運用するための互換性維持とバージョン戦略を整理します。デフォルト引数とオーバーロードの注意、v1/v2ラッパー、非推奨化、JSON契約、ALL_ARGUMENTS によるシグネチャ比較、Edition-Based Redefinition、回帰テストまでを実務向けにまとめます。

先に結論

  • パッケージ仕様部は契約です。既存の引数型、戻り値型、例外、トランザクション方針を安易に変えません。
  • デフォルト引数付き関数と同じ呼び出し形になるオーバーロードは、曖昧になるため避けます。
  • 破壊的変更は同じ仕様部へ押し込まず、v2パッケージと互換ラッパーを併存させます。
  • JSONは文字列連結せず、JSON_OBJECT で生成し、契約バージョンを含めます。
  • EBRを使う場合も、表やシーケンスなど非エディション化オブジェクトとの境界を設計します。

モジュール境界の考え方は 大規模システムにおけるモジュール分割と依存制御、APIの権限境界は マルチスキーマ環境での権限管理とInvoker Rights設計、EBRの詳細は EBRの使い方 と合わせて読むと整理しやすくなります。

スポンサーリンク

互換性はシグネチャだけではない

API互換性というと引数や戻り値だけを見がちですが、実務では複数の互換性があります。コンパイルが通るだけでは十分ではありません。

  • シグネチャ互換性: 名前、引数数、型、モード、戻り値型が既存呼び出しを壊さないか
  • 振る舞い互換性: 同じ入力で同じ意味の結果になるか
  • 副作用互換性: INSERT、UPDATE、ログ、通知の発生条件が変わっていないか
  • トランザクション互換性: COMMIT、ROLLBACK、SAVEPOINTの境界が変わっていないか
  • 例外互換性: エラーコードや例外の発生条件が変わっていないか
  • 権限互換性: AUTHIDや必要GRANTが変わっていないか

安全な拡張と危険な変更を分ける

既存APIの末尾へデフォルト引数を追加する方法は、位置指定の既存呼び出しを維持しやすい一方、同じ呼び出し形になるオーバーロードと併用すると曖昧になります。

avoid-ambiguous-overload.sql
-- 危険: 2引数呼び出しが両方に一致し得る
CREATE OR REPLACE PACKAGE order_api_bad AS
  FUNCTION place(
    p_customer_id IN NUMBER,
    p_amount      IN NUMBER,
    p_note        IN VARCHAR2 DEFAULT NULL
  ) RETURN NUMBER;

  FUNCTION place(
    p_customer_id IN NUMBER,
    p_amount      IN NUMBER
  ) RETURN NUMBER;
END order_api_bad;
/

-- 安全寄り: 公開シグネチャを1つに絞る
CREATE OR REPLACE PACKAGE order_api_v2 AS
  FUNCTION place(
    p_customer_id IN NUMBER,
    p_amount      IN NUMBER,
    p_note        IN VARCHAR2 DEFAULT NULL
  ) RETURN NUMBER;
END order_api_v2;
/

既存の2引数APIを残したい場合は、同じパッケージ内で曖昧なオーバーロードを増やすのではなく、v1ラッパーを残してv2へ委譲します。呼び出し元は段階的にv2へ移行できます。

v1ラッパーからv2へ委譲する

破壊的変更を同じ仕様部で行わず、旧版と新版を併存させます。v1は契約を変えずに残し、内部実装だけをv2へ委譲します。

versioned-api-wrapper.sql
CREATE OR REPLACE PACKAGE order_api_v2 AUTHID DEFINER AS
  FUNCTION place(
    p_customer_id IN NUMBER,
    p_amount      IN NUMBER,
    p_note        IN VARCHAR2,
    p_request_id  IN VARCHAR2
  ) RETURN NUMBER;
END order_api_v2;
/

CREATE OR REPLACE PACKAGE order_api_v1 AUTHID DEFINER AS
  FUNCTION place(
    p_customer_id IN NUMBER,
    p_amount      IN NUMBER
  ) RETURN NUMBER;
END order_api_v1;
/

CREATE OR REPLACE PACKAGE BODY order_api_v1 AS
  FUNCTION place(
    p_customer_id IN NUMBER,
    p_amount      IN NUMBER
  ) RETURN NUMBER IS
  BEGIN
    RETURN order_api_v2.place(
      p_customer_id => p_customer_id,
      p_amount      => p_amount,
      p_note        => NULL,
      p_request_id  => RAWTOHEX(SYS_GUID())
    );
  END;
END order_api_v1;
/

この方法なら、v1利用者はすぐに変更する必要がありません。新しい利用者だけv2へ移し、利用状況を計測しながらv1の廃止時期を決められます。ラッパー側では、旧版のデフォルト動作を明示しておくことが重要です。

安定名のファサードを設ける

利用者にバージョン名を意識させたくない場合は、安定名のファサードを置きます。ただし、ファサード仕様部を頻繁に変えると依存先がINVALIDになるため、安定名には長期維持する最小限の契約だけを置きます。

stable-api-facade.sql
CREATE OR REPLACE PACKAGE order_api AUTHID DEFINER AS
  FUNCTION place(
    p_customer_id IN NUMBER,
    p_amount      IN NUMBER
  ) RETURN NUMBER;
END order_api;
/

CREATE OR REPLACE PACKAGE BODY order_api AS
  FUNCTION place(
    p_customer_id IN NUMBER,
    p_amount      IN NUMBER
  ) RETURN NUMBER IS
  BEGIN
    RETURN order_api_v2.place(
      p_customer_id => p_customer_id,
      p_amount      => p_amount,
      p_note        => NULL,
      p_request_id  => RAWTOHEX(SYS_GUID())
    );
  END;
END order_api;
/

ファサード本体の委譲先だけを変えれば、仕様部の依存を広げずに実装を更新できます。ただし、新機能を全て安定名へ追加すると再び仕様部が肥大化します。新契約はv2など別名で公開し、安定名は互換契約に限定する設計も有効です。

非推奨化は利用状況を測ってから進める

旧APIを廃止する前に、誰がどれだけ使っているかを計測します。警告ログ、相関ID、MODULE/ACTIONを残し、移行対象を特定します。

deprecated-api-wrapper.sql
CREATE OR REPLACE PACKAGE BODY order_api_v1 AS
  FUNCTION place(
    p_customer_id IN NUMBER,
    p_amount      IN NUMBER
  ) RETURN NUMBER IS
  BEGIN
    pkg_logger.info(
      p_message     => 'DEPRECATED order_api_v1.place used',
      p_target_type => 'PACKAGE_API',
      p_target_key  => 'ORDER_API_V1.PLACE',
      p_payload     => JSON_OBJECT(
        'customer_id' VALUE p_customer_id,
        'module' VALUE SYS_CONTEXT('USERENV', 'MODULE'),
        'action' VALUE SYS_CONTEXT('USERENV', 'ACTION')
        RETURNING CLOB
      )
    );

    RETURN order_api_v2.place(
      p_customer_id => p_customer_id,
      p_amount      => p_amount,
      p_note        => NULL,
      p_request_id  => RAWTOHEX(SYS_GUID())
    );
  END;
END order_api_v1;
/

非推奨ログは毎回大量に出すと負荷になります。利用者やモジュール単位でサンプリングする、日次集計へまとめるなど、計測コストを抑えます。ロギングと相関IDの設計は ロギングとトレーサビリティを両立する監査設計 も参考になります。

JSON契約はJSON_OBJECTで生成する

外部アプリケーションや疎結合な呼び出し元には、JSONで契約を返す方法もあります。ただし、文字列連結や DBMS_ASSERT.ENQUOTE_LITERAL はJSON生成用途ではありません。JSON_OBJECT を使い、契約バージョンを含めます。

versioned-json-response.sql
CREATE OR REPLACE FUNCTION place_order_json(
  p_customer_id IN NUMBER,
  p_amount      IN NUMBER,
  p_note        IN VARCHAR2 DEFAULT NULL
) RETURN CLOB IS
  v_order_id NUMBER;
BEGIN
  v_order_id := order_api_v2.place(
    p_customer_id => p_customer_id,
    p_amount      => p_amount,
    p_note        => p_note,
    p_request_id  => RAWTOHEX(SYS_GUID())
  );

  RETURN JSON_OBJECT(
    'schema_version' VALUE 2,
    'ok' VALUE TRUE,
    'order_id' VALUE v_order_id,
    'error_code' VALUE NULL,
    'error_detail' VALUE NULL
    RETURNING CLOB
  );
EXCEPTION
  WHEN OTHERS THEN
    RETURN JSON_OBJECT(
      'schema_version' VALUE 2,
      'ok' VALUE FALSE,
      'order_id' VALUE NULL,
      'error_code' VALUE 'E-ORDER-001',
      'error_detail' VALUE SQLERRM
      RETURNING CLOB
    );
END;
/

既存利用者が未知フィールドを無視できる契約にしておけば、フィールド追加は比較的安全です。一方、フィールド名変更、型変更、意味変更は破壊的変更です。JSONでもスキーマバージョンと移行ルールが必要です。

ALL_ARGUMENTSで公開シグネチャを比較する

仕様部の変更を人のレビューだけに任せず、ALL_ARGUMENTS をスナップショットして差分を確認します。CIで旧版と新版の引数一覧を比較すると、意図しない型変更や必須化を検出できます。

snapshot-public-signatures.sql
SELECT
  owner,
  package_name,
  object_name AS subprogram_name,
  overload,
  subprogram_id,
  sequence,
  position,
  argument_name,
  in_out,
  data_type,
  type_owner,
  type_name,
  defaulted
FROM all_arguments
WHERE owner = UPPER(:api_owner)
  AND package_name IN ('ORDER_API_V1', 'ORDER_API_V2', 'ORDER_API')
ORDER BY package_name, subprogram_id, sequence;

差分判定では、引数名、順序、型、IN/OUT、戻り値、デフォルト有無を確認します。名前付き記法を使う呼び出し元がある場合、引数名変更も破壊的変更になります。

依存範囲とINVALIDを事前確認する

パッケージ仕様部を変更すると、参照しているオブジェクトがINVALIDになる可能性があります。変更前に依存先を抽出し、再コンパイルと回帰テストの対象を決めます。

check-api-dependents.sql
SELECT
  owner,
  name,
  type,
  referenced_owner,
  referenced_name,
  referenced_type
FROM all_dependencies
WHERE referenced_owner = UPPER(:api_owner)
  AND referenced_name = UPPER(:package_name)
ORDER BY owner, type, name;

SELECT owner, object_type, object_name, status
FROM all_objects
WHERE owner = UPPER(:api_owner)
  AND status = 'INVALID'
ORDER BY object_type, object_name;

依存関係の可視化と再コンパイル順序は パッケージ間依存を可視化するメタデータ解析依存オブジェクトとINVALID再コンパイルの制御 で詳しく扱っています。

EBRで新旧版を並行稼働する

停止時間を抑えてパッケージを切り替える場合、Edition-Based Redefinitionを使えます。ただし、パッケージやビューなどのエディション化オブジェクトと、表やシーケンスなど基本的に非エディション化のオブジェクトを分けて考えます。

ebr-deployment-flow.sql
-- 管理権限を持つユーザーが事前に実行
ALTER USER app ENABLE EDITIONS;

CREATE EDITION release_202606 AS CHILD OF ORA$BASE;

-- デプロイセッションを新エディションへ切り替える
ALTER SESSION SET EDITION = release_202606;

-- 新エディション内へパッケージをデプロイ
CREATE OR REPLACE PACKAGE BODY app.order_api AS
  -- 新実装
END order_api;
/

-- セッション単位で検証
ALTER SESSION SET EDITION = release_202606;
SELECT SYS_CONTEXT('USERENV', 'CURRENT_EDITION_NAME') FROM dual;

-- 検証後、必要な管理権限でデフォルトエディションを切り替える
ALTER DATABASE DEFAULT EDITION = release_202606;

ALTER USERCREATE EDITIONALTER DATABASE DEFAULT EDITION には強い権限が必要です。アプリケーションの通常デプロイユーザーへ安易に付与せず、DBA手順として分離します。

表構造を同時に変える場合は、エディショニング・ビューやクロスエディション・トリガを使い、新旧コードが同じ非エディション化表を安全に扱えるようにします。CI/CDとの組み合わせは コードデプロイの自動化とEdition管理 も参考になります。

互換テストは副作用を隔離する

旧APIと新APIを同じデータへ連続実行すると、注文や更新が二重に作られます。互換テストでは、テスト専用スキーマ、テスト専用キー、ROLLBACK可能なAPI、冪等なrequest_idなどを使って副作用を隔離します。

compatibility-test-with-rollback.sql
DECLARE
  v_order_id_v1 NUMBER;
  v_order_id_v2 NUMBER;
BEGIN
  SAVEPOINT before_compat_test;

  DBMS_APPLICATION_INFO.SET_MODULE('API_COMPAT_TEST', 'ORDER_PLACE');

  v_order_id_v1 := order_api_v1.place(
    p_customer_id => 900001,
    p_amount      => 1000
  );

  -- v2は別のテスト顧客・一意なrequest_idで検証する
  v_order_id_v2 := order_api_v2.place(
    p_customer_id => 900002,
    p_amount      => 1000,
    p_note        => NULL,
    p_request_id  => 'TEST-' || RAWTOHEX(SYS_GUID())
  );

  IF v_order_id_v1 IS NULL OR v_order_id_v2 IS NULL THEN
    RAISE_APPLICATION_ERROR(-20001, 'order_id is null');
  END IF;

  -- 件数、金額、監査ログ、例外コードなどを比較する
  ROLLBACK TO before_compat_test;
END;
/

このテストは、API内部でCOMMITしない設計を前提にしています。API内部でCOMMITする場合、SAVEPOINTでは戻せません。その場合はテスト専用DB、テストデータの削除手順、request_idによる冪等化など、別の隔離策が必要です。

リリース手順を定型化する

  • 現在の公開シグネチャをALL_ARGUMENTSで保存する
  • 依存先とINVALID化する可能性があるオブジェクトを抽出する
  • v2と互換ラッパーを先にデプロイする
  • 旧APIの利用状況を警告ログで計測する
  • 代表利用者で互換テストと権限テストを行う
  • EBR利用時は新旧エディションで並行検証する
  • 旧版廃止日と移行ガイドを明示する
  • ロールバック条件とデータ整合手順を決める

まとめ

PL/SQLパッケージAPIの互換性は、引数や戻り値だけでなく、振る舞い、副作用、トランザクション、例外、権限まで含めた契約です。デフォルト引数と曖昧なオーバーロードを併用せず、破壊的変更はv2パッケージと互換ラッパーで段階移行します。

JSON契約は JSON_OBJECT で安全に生成し、ALL_ARGUMENTSALL_DEPENDENCIES で変更影響を機械的に確認します。停止時間を抑える必要があればEBRを使い、非エディション化表との境界と強い管理権限を別途設計します。利用計測、互換テスト、廃止日まで含めて初めて、長期運用できるバージョン戦略になります。