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

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

パッケージAPIは一度公開すると利用側のコードが増殖し、わずかな仕様変更が全社的なコンパイル失敗や想定外の副作用を引き起こす。大規模なPL/SQL環境では、互換性を壊さずに進化させるための契約設計と移行の手順、さらにEdition-Based Redefinitionやシノニム切替を活用した段階的リリース戦略が不可欠である。本稿では、シグネチャ凍結、互換オーバーロード、段階的非推奨化、バージョン交渉、エディション切替の組み合わせにより、長期運用で破壊的変更を吸収する実践的方法を解説する。

契約としてのAPIとシグネチャ凍結

APIは仕様部が契約であり、互換性はシグネチャ(名前・パラメータ数と型・戻り値)から定義される。最小限の原則は、公開後の関数に対し既存パラメータの型変更や必須化を行わないことであり、拡張は引数の末尾への追加を原則とし、デフォルト値によって既存呼び出しを壊さないようにする。戻り値型の拡張が必要ならば、レコード型やJSON文字列のような拡張可能なコンテナで契約面を安定化させる。

セマンティックバージョニングと互換オーバーロード

APIの進化はMAJOR.MINOR.PATCHを用いて表現し、MINOR/PATCHは後方互換を前提にする。PL/SQLではオーバーロードとデフォルト引数でMINORの互換拡張を実現し、MAJOR相当の破壊的変更は別名のパッケージ(_v2など)として併存させる。次の例は、v1の既存シグネチャを維持しつつ、v2でオプション引数を追加して互換オーバーロードする構成である。


-- v1: 既存契約(シグネチャ凍結)
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
    v_order_id NUMBER;
  BEGIN
    INSERT INTO orders(order_id, customer_id, amount) 
    VALUES (orders_seq.NEXTVAL, p_customer_id, p_amount)
    RETURNING order_id INTO v_order_id;
    RETURN v_order_id;
  END;
END order_api_v1;
/
-- v2: 後方互換の拡張(末尾にオプション引数)
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 DEFAULT NULL) RETURN NUMBER;
  FUNCTION place(p_customer_id IN NUMBER, p_amount IN NUMBER) RETURN NUMBER; -- 互換オーバーロード
END order_api_v2;
/
CREATE OR REPLACE PACKAGE BODY order_api_v2 AS
  FUNCTION place(p_customer_id IN NUMBER, p_amount IN NUMBER, p_note IN VARCHAR2 DEFAULT NULL) RETURN NUMBER IS
    v_order_id NUMBER;
  BEGIN
    INSERT INTO orders(order_id, customer_id, amount, note) 
    VALUES (orders_seq.NEXTVAL, p_customer_id, p_amount, p_note)
    RETURNING order_id INTO v_order_id;
    RETURN v_order_id;
  END;
  FUNCTION place(p_customer_id IN NUMBER, p_amount IN NUMBER) RETURN NUMBER IS
  BEGIN
    RETURN place(p_customer_id, p_amount, NULL);
  END;
END order_api_v2;
/

ファサードとシノニムによる進化の吸収

利用側を直接バージョン名に結び付けると将来の切替が困難になるため、安定名のファサードを常に用意し、内部で最新版へ委譲する。切替は仕様互換を保ちながらファサードの委譲先を変更し、最終段でシノニムを更新する。


-- 安定名のファサード(互換インターフェースのみ露出)
CREATE OR REPLACE PACKAGE order_api AUTHID DEFINER AS
  FUNCTION place(p_customer_id IN NUMBER, p_amount IN NUMBER, p_note IN VARCHAR2 DEFAULT NULL) RETURN NUMBER;
END order_api;
/
CREATE OR REPLACE PACKAGE BODY order_api AS
  FUNCTION place(p_customer_id IN NUMBER, p_amount IN NUMBER, p_note IN VARCHAR2 DEFAULT NULL) RETURN NUMBER IS
  BEGIN
    RETURN order_api_v2.place(p_customer_id, p_amount, p_note);
  END;
END order_api;
/
-- 互換維持のまま将来 v3 へ差し替えるときは、BODY内の委譲先を変更するだけでよい

非推奨化と移行のガードレール

破壊的変更へ移行する際は、まず古いエントリポイントを互換ファサード内でラップし、ログに非推奨警告を残して利用状況を可視化する。非推奨段階では機能を温存し、移行ガイドに沿って利用側を段階的に置換する。最終段で旧版を切り離す際は、エラーコードを統一して早期に失敗させ、問い合わせ対応を容易にする。


-- 非推奨APIのラップ例(利用検知と警告ログ)
CREATE OR REPLACE PACKAGE order_api_legacy AUTHID DEFINER AS
  FUNCTION place(p_customer_id IN NUMBER, p_amount IN NUMBER) RETURN NUMBER;
END order_api_legacy;
/
CREATE OR REPLACE PACKAGE BODY order_api_legacy AS
  FUNCTION place(p_customer_id IN NUMBER, p_amount IN NUMBER) RETURN NUMBER IS
  BEGIN
    pkg_logger.log_info('DEPRECATED order_api_legacy.place used', NULL);
    RETURN order_api.place(p_customer_id, p_amount, NULL); -- 新APIへ委譲
  END;
END order_api_legacy;
/

入力・出力契約の拡張容易性とエラー設計

将来の拡張を見据えて、レコード型やJSONを返す関数を併設すると互換性を保ちやすい。追加フィールドは無視可能であるべきで、既存の利用側が壊れないようにパース時の既定値を定める。エラーは例外メッセージ任せにせず、ドメイン固有のエラーコードを戻り値やJSONプロパティで返す関数を併置すると、非例外フローでの後方互換が維持できる。


CREATE OR REPLACE PACKAGE order_types AS
  TYPE t_order_json IS RECORD(
    ok           NUMBER,        -- 1=成功, 0=失敗
    order_id     NUMBER,
    error_code   VARCHAR2(64),
    error_detail VARCHAR2(2000),
    payload_json CLOB
  );
END order_types;
/
CREATE OR REPLACE PACKAGE order_api_ext AUTHID DEFINER AS
  FUNCTION place_json(p_customer_id IN NUMBER, p_amount IN NUMBER, p_note IN VARCHAR2 DEFAULT NULL) RETURN CLOB;
END order_api_ext;
/
CREATE OR REPLACE PACKAGE BODY order_api_ext AS
  FUNCTION place_json(p_customer_id IN NUMBER, p_amount IN NUMBER, p_note IN VARCHAR2 DEFAULT NULL) RETURN CLOB IS
    v_id NUMBER;
  BEGIN
    v_id := order_api.place(p_customer_id, p_amount, p_note);
    RETURN '{"ok":1,"order_id":'||v_id||',"error_code":null,"error_detail":null,"payload_json":null}';
  EXCEPTION
    WHEN OTHERS THEN
      RETURN '{"ok":0,"order_id":null,"error_code":"E-ORDER-001","error_detail":' ||
             DBMS_ASSERT.ENQUOTE_LITERAL(SQLERRM) || ',"payload_json":null}';
  END;
END order_api_ext;
/

Edition-Based Redefinitionとローリングリリース

ダウンタイムを抑えるにはEdition-Based Redefinition(EBR)で版を切り替える。新エディションで新パッケージをデプロイし、セッションは各自のエディションで安定稼働する。十分な検証後にデフォルトエディションを切り替え、必要ならエディショニング・ビューとクロスエディション・トリガでスキーマ進化(列追加・型拡張)を吸収する。


-- 例: EBR の基本フロー(権限設定は省略)
ALTER USER app ENABLE EDITIONS;
CREATE EDITION v2 AS CHILD OF ORA$BASE;
-- v2 に切替えて新APIを配置
ALTER SESSION SET EDITION = v2;
-- order_api_v2 をこのエディションにデプロイ(前掲の定義)
-- 旧セッションは ORA$BASE のまま v1 を参照し続ける
-- 検証後、既定エディションを切替
ALTER DATABASE DEFAULT EDITION = v2;

互換テストとリグレッション防止

互換性は運用前に自動化テストで担保する必要がある。既存シグネチャに対しては回帰テストを固定化し、新旧で同一入力に対する観測可能な挙動(戻り値、コミット結果、ログ副作用)が等価であることを検証する。可視化のためにDBMS_APPLICATION_INFOでMODULE/ACTIONを固定し、トレースとログを相関IDで縦断確認できるようにしておく。


-- 擬似的な回帰テスト例(テストフレームワークは省略)
DECLARE
  v1 NUMBER; v2 NUMBER;
BEGIN
  DBMS_APPLICATION_INFO.set_module('TEST','ORDER_PLACE');
  v1 := order_api_v1.place(10, 1000);
  v2 := order_api.place(10, 1000, NULL); -- v2 経由
  IF v1 IS NULL OR v2 IS NULL THEN
    RAISE_APPLICATION_ERROR(-20001, 'order_id is null');
  END IF;
  -- 追加で整合性を検証(件数、金額、ログ記録の有無など)
END;
/

リリース手順の定型化とまとめ

互換性を守るリリースは、仕様部の凍結、互換オーバーロードでの拡張、ファサード/シノニムによる差し替え、非推奨フェーズでの利用計測、最終的な切替という段階を踏む。ゼロダウンタイムが要請される場合はEBRで並走させ、既定エディションを切り替える。エラー設計とJSON/レコードによる拡張可能な契約、そして自動化された互換テストを併用すれば、PL/SQLでもMAJORを恐れずに進化できる。長期運用では、安定名のファサードを終点に据え、内部実装を段階的に更新する戦略が、互換性維持と開発速度の両立をもたらす。