【PL/SQL】コードデプロイの自動化とEdition管理(CI/CD連携)

【PL/SQL】コードデプロイの自動化とEdition管理(CI/CD連携) PL/SQL

PL/SQLのデプロイをCI/CDへ組み込むとき、単純に CREATE OR REPLACE PACKAGE を流すだけでは、接続中セッション、依存オブジェクト、スキーマ変更、切り戻しの問題が残ります。OracleのEdition-Based Redefinition(EBR)を使うと、新旧のPL/SQLやビューをEdition単位で共存させ、既存セッションを止めずに段階切替できます。

この記事では、DBA初期設定と通常デプロイを分離し、Edition作成、Editioning View、Crossedition Trigger、SQLclランナー、品質ゲート、フェイルバックまでを実務の順序で整理します。EBR単体の基本は PL/SQLのEBR完全ガイド、INVALID再コンパイルは 依存オブジェクトとINVALID再コンパイルの制御 もあわせて確認してください。

この記事で扱うこと

  • CI/CDで毎回実行する処理と、DBAが一度だけ行う初期設定を分ける
  • CREATE EDITION ... AS CHILD OF ... で新Editionを作る
  • Editioning Viewで表構造の変更を吸収する
  • Crossedition Triggerで切替期間中のデータ整合を維持する
  • SQLclでデプロイ順序を固定し、品質ゲートで失敗を検出する
  • 問題発生時に旧Editionへフェイルバックする
スポンサーリンク

DBA初期設定と通常デプロイを分離する

ALTER USER ... ENABLE EDITIONS は毎回のCI/CDで実行する処理ではありません。これはスキーマをEdition対応にする初期設定であり、通常はDBA作業として一度だけ実行します。CI/CDの通常デプロイでは、新Editionを作成し、そのEditionへアプリケーションオブジェクトをコンパイルします。

ebr-initial-setup-dba.sql
-- DBAまたは権限を持つ管理者が一度だけ実行する初期設定
ALTER USER app_user ENABLE EDITIONS;

-- Edition利用を明示的に許可する運用の場合
GRANT USE ON EDITION ora$base TO app_user;

-- 以後のデプロイでは、通常この初期設定を繰り返さない

初期設定をCIランナーに混ぜると、不要な権限をCIに渡すことになり、監査上も危険です。CIユーザーには、通常デプロイに必要な範囲だけを付与し、DBA権限や全体設定の変更権限は分離します。

新Editionを作成してセッションを切り替える

通常のデプロイでは、親Editionを指定して子Editionを作成し、以後のDDLをそのEdition上で実行します。Oracleの構文は CREATE EDITION app_v2 AS CHILD OF app_v1 です。既存記事やメモで UNDER のような表現を見かけても、そのまま使わないよう注意してください。

create-edition-and-switch.sql
-- 例: app_v1 を親にして app_v2 を作る
CREATE EDITION app_v2 AS CHILD OF app_v1;

-- この接続で以後のコンパイル先を app_v2 にする
ALTER SESSION SET EDITION = app_v2;

-- 確認
SELECT SYS_CONTEXT('USERENV', 'CURRENT_EDITION_NAME') AS current_edition
  FROM dual;

Edition名はリリース番号や日時を含めて機械的に作ると運用しやすくなります。ただし、アプリコードの中へEdition名を直接埋め込むと切替が硬くなるため、接続プール、サービス、デプロイ変数などで管理します。

Editioning Viewで表構造の変更を吸収する

表そのものはEdition化されないため、アプリやPL/SQLが実表を直接参照していると、列追加や意味変更の影響を受けやすくなります。公開インターフェースとしてEditioning Viewを置き、Editionごとに見せ方を変えることで、旧Editionと新Editionを並行稼働させやすくなります。

editioning-view-evolution.sql
-- 実表は非Edition化オブジェクト
CREATE TABLE t_order(
  id         NUMBER PRIMARY KEY,
  amount    NUMBER(12,2) NOT NULL,
  created_at TIMESTAMP NOT NULL
);

-- app_v1 の公開インターフェース
ALTER SESSION SET EDITION = app_v1;

CREATE OR REPLACE EDITIONING VIEW v_order AS
SELECT id, amount, created_at
  FROM t_order;

-- 後方互換の実表拡張
ALTER TABLE t_order ADD (
  currency CHAR(3) DEFAULT 'JPY' NOT NULL
);

-- app_v2 では新列も公開する
ALTER SESSION SET EDITION = app_v2;

CREATE OR REPLACE EDITIONING VIEW v_order AS
SELECT id, amount, created_at, currency
  FROM t_order;

アプリケーションやPL/SQLは t_order を直接参照せず、v_order を参照する方針に統一します。これにより、表の拡張とアプリの切替タイミングを分離できます。

Crossedition Triggerで切替期間中の整合を保つ

列の追加だけならEditioning Viewで吸収できますが、列の分割、正規化、意味変更を伴う場合は、旧EditionからのDMLと新Editionのデータ構造を橋渡しする必要があります。この期間だけ使うのがCrossedition Triggerです。長期間残すものではなく、切替が終わったら撤去する前提で設計します。

forward-crossedition-trigger.sql
-- 例: app_v2で fee 列を追加し、旧 amount から補完する
ALTER TABLE t_order ADD (
  fee NUMBER(12,2) DEFAULT 0 NOT NULL
);

-- 切替期間中、旧インターフェースからのDMLを新列へ補完する
CREATE OR REPLACE TRIGGER t_order_fee_fwd
  BEFORE INSERT OR UPDATE OF amount ON t_order
  FOR EACH ROW
  FORWARD CROSSEDITION
BEGIN
  IF :NEW.fee IS NULL OR :NEW.fee = 0 THEN
    :NEW.fee := ROUND(:NEW.amount * 0.03, 2);
  END IF;
END;
/

双方向に戻す必要がある期間は、forward/reverseの設計を明確にし、どちらのEditionから書かれたデータも整合するかを検証します。CETは通常のDMLパスに乗るため、負荷と実行期間を短くすることも重要です。

PL/SQLのコンパイル順序を固定する

CI/CDでは、ファイル名や実行順が毎回ぶれないようにします。表拡張、Editioning View、Crossedition Trigger、パッケージSPEC、パッケージBODY、再コンパイル、スモークテストの順に流すと、依存関係の破損を検出しやすくなります。コンパイルエラー検出は PL/SQLコンパイル時エラーと警告の対処、警告をCIで厳格化する方法は DBMS_WARNINGとPLSQL_WARNINGSの使い方 が関連します。

deploy-order.sql
-- 01: 後方互換の表拡張
@01_tables_additive.sql

-- 02: Editioning Viewを新Editionで更新
@02_editioning_views.sql

-- 03: 必要な期間だけCrossedition Triggerを追加
@03_crossedition_triggers.sql

-- 10: 公開仕様を先にコンパイル
@10_package_specs.sql

-- 11: 実装を後からコンパイル
@11_package_bodies.sql

-- 90: 再コンパイルと品質ゲート
@90_quality_gate.sql

SQLclでCI/CDランナーを作る

CI/CDからSQLclを非対話で実行する場合は、エラー時に必ずプロセスを失敗させます。また、Edition有効化のようなDBA初期設定はここに含めず、通常デプロイに必要な処理だけを実行します。

sqlcl-deploy-runner.sql
-- deploy.sql
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET SERVEROUTPUT ON

ALTER SESSION SET EDITION = app_v2;

@01_tables_additive.sql
@02_editioning_views.sql
@03_crossedition_triggers.sql
@10_package_specs.sql
@11_package_bodies.sql
@90_quality_gate.sql

EXIT 0
github-actions-sqlcl.yml
name: deploy-plsql

on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Deploy PL/SQL to new edition
        run: |
          sql app_user/${DB_PASSWORD}@${DB_CONNECT} @deploy.sql
        env:
          DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
          DB_CONNECT: ${{ secrets.DB_CONNECT }}

本格運用では、DB接続情報をGitHub Actions Secretsなどに入れ、ログにパスワードが出ないようにします。ジョブの履歴や異常検知をDB側にも残す場合は DBMS_SCHEDULERとロギングによるジョブ実行履歴管理 が参考になります。

品質ゲートSQLで失敗を自動検出する

デプロイ後に「SQLclは成功したが、実はINVALIDが残っていた」という状態を避けるため、品質ゲートSQLを必ず置きます。標準出力の文字列解析だけに頼らず、Oracleのディクショナリビューを見て明示的に失敗させます。

quality-gate-invalid-errors.sql
SET SERVEROUTPUT ON

DECLARE
  v_invalid_count NUMBER;
  v_error_count   NUMBER;
BEGIN
  SELECT COUNT(*)
    INTO v_invalid_count
    FROM user_objects
   WHERE status = 'INVALID';

  SELECT COUNT(*)
    INTO v_error_count
    FROM user_errors;

  IF v_invalid_count > 0 OR v_error_count > 0 THEN
    DBMS_OUTPUT.PUT_LINE('INVALID objects: ' || v_invalid_count);
    DBMS_OUTPUT.PUT_LINE('USER_ERRORS: ' || v_error_count);
    RAISE_APPLICATION_ERROR(-20001, 'PL/SQL deploy quality gate failed');
  END IF;
END;
/
quality-gate-detail.sql
SELECT object_name, object_type, status
  FROM user_objects
 WHERE status = 'INVALID'
 ORDER BY object_type, object_name;

SELECT name, type, line, position, text
  FROM user_errors
 ORDER BY name, sequence;

SELECT name, type, referenced_name, referenced_type
  FROM user_dependencies
 WHERE referenced_name IN ('V_ORDER', 'T_ORDER')
 ORDER BY name, referenced_name;

実行時の観測性を高めるには、デプロイIDやリリース番号を DBMS_APPLICATION_INFO で載せるのも有効です。詳しくは DBMS_APPLICATION_INFOを使ったインストゥルメンテーション設計 を参照してください。

接続プールとEdition切替の扱い

新Editionを作っても、既存セッションが自動的に新Editionへ移るわけではありません。アプリケーションサーバの接続プール、接続時の初期化SQL、サービス設定などで、新規接続をどのEditionへ向けるかを制御します。既存セッションは旧Editionで完走させ、新規セッションから段階的に新Editionへ流すのが安全です。

connection-pool-edition-switch.sql
-- 接続プールの初期化SQLなどで実行する例
ALTER SESSION SET EDITION = app_v2;

-- アプリ起動直後の確認SQL
SELECT SYS_CONTEXT('USERENV', 'CURRENT_EDITION_NAME') AS current_edition
  FROM dual;

AFTER LOGON ON DATABASE トリガで強制的にEditionを切り替える方法もありますが、影響範囲が大きいため慎重に扱います。使う場合は対象ユーザーや接続元を厳密に絞り、管理者接続やメンテナンス処理を巻き込まないようにします。

guarded-logon-trigger.sql
CREATE OR REPLACE TRIGGER set_app_edition_on_login
AFTER LOGON ON DATABASE
BEGIN
  IF SYS_CONTEXT('USERENV', 'SESSION_USER') = 'APP_USER'
     AND SYS_CONTEXT('USERENV', 'MODULE') LIKE 'app-api%' THEN
    EXECUTE IMMEDIATE 'ALTER SESSION SET EDITION = app_v2';
  END IF;
END;
/

実務では、全DBログオントリガより、アプリ側の接続初期化やサービス単位の制御を優先する方が事故範囲を小さくできます。

スモークテストとウォームアップを入れる

Edition切替直後は、主要パッケージやビューを軽く実行して、コンパイル漏れや権限漏れを早期に検出します。高負荷な処理ではなく、代表的なAPIを最小入力で呼ぶスモークテストを用意します。

smoke-test-new-edition.sql
ALTER SESSION SET EDITION = app_v2;

BEGIN
  DBMS_APPLICATION_INFO.SET_MODULE(
    module_name => 'plsql-deploy',
    action_name => 'smoke-test-app-v2'
  );

  -- 主要ビューを軽く参照
  DECLARE
    v_count NUMBER;
  BEGIN
    SELECT COUNT(*) INTO v_count FROM v_order WHERE ROWNUM <= 1;
  END;

  -- 主要パッケージの疎通確認
  app_order_api.health_check;
END;
/

スモークテストで一時障害を扱う場合は、無制限リトライにせず、回数と対象例外を限定します。例外分類は PL/SQLの例外設計と再試行パターン と組み合わせると整理しやすくなります。

フェイルバックとロールバックを分けて考える

EBRでは、問題発生時に新規接続を旧Editionへ戻すだけで、コード面のフェイルバックができます。一方、データレイアウト変更を完全に戻すロールバックは別問題です。破壊的変更を行う場合は、CETで双方向整合を維持できる期間を明確にし、その期間内だけ安全に戻せるようにします。

fallback-to-previous-edition.sql
-- アプリ接続の初期化SQLを旧Editionへ戻す
ALTER SESSION SET EDITION = app_v1;

-- 既存の新Editionセッションが残っていないか確認する
SELECT username, edition_name, status, module
  FROM v$session
 WHERE username = 'APP_USER';

-- 問題切り分けが終わるまで、新EditionやCETは急いでDROPしない

フェイルバック後に、旧EditionからのDMLが新列や新構造と矛盾しないかを確認します。CETや追加列を撤去するのは、全セッション移行、監査ログ確認、データ整合確認が終わってからです。

よくある落とし穴と回避策

CIでALTER USER ENABLE EDITIONSを毎回流す初期設定と通常デプロイを分け、CI権限を最小化します。
実表をアプリから直接参照するEditioning Viewを公開インターフェースにして、表変更の影響を吸収します。
SPECを頻繁に変える依存再コンパイルが増えるため、公開仕様は薄く安定させ、BODY中心で差し替えます。
CETを長期間残すDMLコストと複雑性が増えるため、切替期間だけ使い、撤去条件を決めておきます。
ログオントリガで全ユーザーを巻き込む対象ユーザー、MODULE、接続元を絞るか、接続プール初期化SQLで制御します。
INVALID確認を人手に任せるUSER_OBJECTSとUSER_ERRORSを品質ゲートにして、CIを失敗させます。

まとめ

PL/SQLのコードデプロイをCI/CD化するなら、EBRを軸にして、Editioning Viewで公開インターフェースを安定させ、必要な期間だけCrossedition Triggerでデータ整合を橋渡しするのが基本です。DBA初期設定と通常デプロイを分離し、CIでは新Editionへのコンパイル、品質ゲート、スモークテストまでを自動化します。

重要なのは、デプロイを「DDLを流す作業」ではなく、「新Editionを作り、依存整合を検証し、新規接続を段階的に切り替え、問題時は旧Editionへ戻せる運用」として設計することです。この形にしておけば、PL/SQLの変更もアプリケーションコードと同じように、監査可能で戻しやすいリリースプロセスへ近づけられます。