【PL/SQL】大規模システムにおけるモジュール分割と依存制御の最適化

PL/SQLを使った業務システムは、最初は数本のパッケージで十分でも、長期運用の中で処理が増え、共通化が進み、いつの間にか依存関係が読めなくなります。変更のたびに別パッケージがINVALIDになる、共通パッケージを直すと全体が再コンパイルされる、循環依存を避けるために動的SQLが増える。こうなると、開発速度よりも調査と再テストの時間が重くなります。

この記事では、大規模PL/SQLシステムでモジュールをどう分け、依存方向をどう制御し、ALL_DEPENDENCIES でどう監査するかを実務目線で整理します。単なる思想ではなく、API層、Service層、DAO層、Common層の切り方、循環依存の検出SQL、公開APIの管理、避けたい動的呼び出しの扱いまで扱います。

先に結論

  • 依存方向は、原則として API / Batch -> Service -> DAO -> Table にそろえます。
  • Common は便利ですが、肥大化すると全体の結合点になります。業務ロジックを入れすぎないことが重要です。
  • 循環依存は EXECUTE IMMEDIATE で隠すより、API境界、共通型、イベント/キュー、責務分割で解消するほうが安全です。
  • ALL_DEPENDENCIES で依存方向、外部スキーマ参照、循環依存、設計違反を定期的に検査します。
  • 権限境界は AUTHID DEFINER / CURRENT_USER と直接GRANTの方針まで含めて設計します。

依存関係の詳しい可視化は パッケージ間依存を可視化するメタデータ解析、INVALIDと再コンパイルは 依存オブジェクトとINVALID再コンパイルの制御、権限境界は AUTHIDと権限管理の設計 と合わせて読むと整理しやすくなります。

スポンサーリンク

大規模PL/SQLで依存が壊れる典型パターン

依存制御が効かなくなる原因は、パッケージ数の多さそのものではありません。境界が曖昧なまま共通化し、どの層からどの層を呼んでよいかを決めないまま拡張することが問題です。

  • 共通パッケージに業務判断が入り、全業務から参照される
  • DAO層からService層を呼び返し、依存方向が逆流する
  • パッケージ仕様部に内部用の型や関数を出しすぎる
  • 循環依存を避けるために動的SQL呼び出しが増える
  • シノニムと権限で依存の実体が追いにくくなる
  • INVALIDになってから初めて依存関係を調べる

設計の目的は「きれいに分けること」ではなく、変更影響範囲を読める状態に保つことです。どのパッケージを直したら、どのテストを回すべきかが即座に判断できる状態を目指します。

推奨する層構造

大規模なPL/SQLでは、名前だけの層分けではなく、依存方向をルールとして決めます。たとえば次のような構成です。

  • API層: 外部から呼ばれる公開入口。入力検証、権限境界、戻り値仕様を安定させます。
  • Service層: 業務ルール、トランザクション方針、複数DAOの組み合わせを扱います。
  • DAO層: テーブル・ビューへのCRUD、SQLの集約、ロック取得を扱います。
  • Common層: 日付、文字列、型、エラーコードなど、業務に依存しない共通部品だけを置きます。
  • Batch / Job層: Schedulerや夜間バッチの入口。APIまたはServiceを呼び、進捗ログを残します。
dependency-direction-rules.txt
許可する依存方向:
  API / Batch  -> Service
  Service      -> DAO
  Service      -> Common
  DAO          -> Table / View / Common
  API          -> Common

避ける依存方向:
  DAO          -> Service
  Common       -> API / Service / DAO
  Service A    -> Service B -> Service A
  Package Body -> 動的SQLで他業務パッケージを直接呼ぶ

共通パッケージは最も便利で、最も危険です。便利だからといって業務判定やテーブル参照を入れると、Commonが全体の中心になり、変更時の影響が爆発します。Commonには、できるだけ純粋な関数、共通定数、共通型、エラーコードなどを置きます。

パッケージ仕様部を公開APIとして扱う

PL/SQLのパッケージ仕様部は、他モジュールから見える契約です。ここに置いた関数、プロシージャ、型、定数は、すべて外部依存の入口になります。逆に、パッケージ本体内のローカル関数は内部実装として隠せます。

pkg-order-api-example.sql
CREATE OR REPLACE PACKAGE pkg_order_api AS
  TYPE t_order_result IS RECORD (
    order_id NUMBER,
    status   VARCHAR2(30),
    message  VARCHAR2(4000)
  );

  FUNCTION create_order(
    p_customer_id IN NUMBER,
    p_order_date  IN DATE
  ) RETURN t_order_result;
END pkg_order_api;
/

CREATE OR REPLACE PACKAGE BODY pkg_order_api AS
  FUNCTION validate_input(
    p_customer_id IN NUMBER,
    p_order_date  IN DATE
  ) RETURN VARCHAR2 IS
  BEGIN
    IF p_customer_id IS NULL THEN
      RETURN 'CUSTOMER_REQUIRED';
    END IF;
    IF p_order_date IS NULL THEN
      RETURN 'ORDER_DATE_REQUIRED';
    END IF;
    RETURN NULL;
  END;

  FUNCTION create_order(
    p_customer_id IN NUMBER,
    p_order_date  IN DATE
  ) RETURN t_order_result IS
    v_error VARCHAR2(100);
    v_order_id NUMBER;
  BEGIN
    v_error := validate_input(p_customer_id, p_order_date);
    IF v_error IS NOT NULL THEN
      RETURN t_order_result(NULL, 'ERROR', v_error);
    END IF;

    v_order_id := pkg_order_service.create_order(
      p_customer_id => p_customer_id,
      p_order_date  => p_order_date
    );

    RETURN t_order_result(v_order_id, 'SUCCESS', NULL);
  END;
END pkg_order_api;
/

外部に公開する仕様は少ないほど保守しやすくなります。内部用の補助関数、SQL組み立て、ログ整形、検証用処理は、仕様部ではなく本体部へ閉じます。プロシージャとファンクションの使い分けは プロシージャ・ファンクション完全ガイド も参考になります。

依存ルールをALL_DEPENDENCIESで監査する

設計ルールはドキュメントに書くだけでは守られません。ALL_DEPENDENCIES で実際の依存関係を定期的に取り、設計違反を検出します。

list-package-dependencies.sql
SELECT
  owner,
  name,
  type,
  referenced_owner,
  referenced_name,
  referenced_type
FROM all_dependencies
WHERE owner = UPPER(:app_owner)
  AND type IN ('PACKAGE', 'PACKAGE BODY', 'PROCEDURE', 'FUNCTION')
  AND referenced_owner IS NOT NULL
ORDER BY owner, name, referenced_owner, referenced_type, referenced_name;
detect-layer-violations.sql
WITH module_rule AS (
  SELECT 'PKG_%_DAO' AS name_pattern, 'DAO' AS layer_name FROM dual UNION ALL
  SELECT 'PKG_%_SERVICE', 'SERVICE' FROM dual UNION ALL
  SELECT 'PKG_%_API', 'API' FROM dual UNION ALL
  SELECT 'PKG_COMMON_%', 'COMMON' FROM dual
),
dep AS (
  SELECT
    owner,
    name,
    referenced_owner,
    referenced_name,
    referenced_type
  FROM all_dependencies
  WHERE owner = UPPER(:app_owner)
    AND referenced_owner = UPPER(:app_owner)
)
SELECT
  d.name AS from_object,
  d.referenced_name AS to_object,
  CASE
    WHEN d.name LIKE 'PKG\_%\_DAO' ESCAPE '\'
     AND d.referenced_name LIKE 'PKG\_%\_SERVICE' ESCAPE '\'
    THEN 'DAOからServiceを参照している'
    WHEN d.name LIKE 'PKG\_COMMON\_%' ESCAPE '\'
     AND d.referenced_name NOT LIKE 'PKG\_COMMON\_%' ESCAPE '\'
    THEN 'Commonから業務パッケージを参照している'
    WHEN d.name LIKE 'PKG\_%\_SERVICE' ESCAPE '\'
     AND d.referenced_name LIKE 'PKG\_%\_API' ESCAPE '\'
    THEN 'ServiceからAPIを参照している'
  END AS violation_reason
FROM dep d
WHERE (
    d.name LIKE 'PKG\_%\_DAO' ESCAPE '\'
    AND d.referenced_name LIKE 'PKG\_%\_SERVICE' ESCAPE '\'
  )
  OR (
    d.name LIKE 'PKG\_COMMON\_%' ESCAPE '\'
    AND d.referenced_name NOT LIKE 'PKG\_COMMON\_%' ESCAPE '\'
  )
  OR (
    d.name LIKE 'PKG\_%\_SERVICE' ESCAPE '\'
    AND d.referenced_name LIKE 'PKG\_%\_API' ESCAPE '\'
  )
ORDER BY violation_reason, from_object, to_object;

上記は命名規約を前提にした簡易チェックです。実際にはモジュール台帳を作り、パッケージ名と層をテーブルで管理すると、より正確に監査できます。命名規約だけで判定すると、例外的な名前や移行途中のパッケージを誤判定するためです。

循環依存を検出する

循環依存は、コンパイル順序やデプロイ手順を複雑にします。特にパッケージ仕様部同士が相互に参照すると、片方の変更がもう片方の再コンパイルを呼び、影響範囲が読みにくくなります。

detect-cyclic-dependencies.sql
WITH dep AS (
  SELECT
    owner,
    name,
    type,
    referenced_owner,
    referenced_name,
    referenced_type
  FROM all_dependencies
  WHERE owner = UPPER(:app_owner)
    AND referenced_owner = UPPER(:app_owner)
    AND type IN ('PACKAGE', 'PACKAGE BODY', 'PROCEDURE', 'FUNCTION')
)
SELECT
  owner || '.' || name AS object_name,
  referenced_owner || '.' || referenced_name AS referenced_object,
  SYS_CONNECT_BY_PATH(owner || '.' || name, ' -> ') AS path_text
FROM dep
WHERE CONNECT_BY_ISCYCLE = 1
CONNECT BY NOCYCLE
       PRIOR referenced_owner = owner
   AND PRIOR referenced_name  = name
   AND PRIOR referenced_type  = type
START WITH owner = UPPER(:app_owner);

循環が見つかった場合、最初に検討するのは動的SQLではありません。まず、共通型を別パッケージへ切り出す、API境界を片方向にする、イベントテーブルやキューで連携する、責務を再分割する、といった静的に追跡できる方法を優先します。

循環依存を解消する安全な選択肢

循環依存を見つけたときの選択肢は複数あります。どれを選ぶかは、呼び出し頻度、トランザクション境界、同期/非同期、権限境界によって変わります。

  • 共通型パッケージへ切り出す: 相互参照の原因が型定義なら、pkg_order_types のような仕様専用パッケージへ寄せます。
  • API層へ依存を集約する: 他業務へ触れる入口をAPIに限定し、下位層からの逆参照を禁止します。
  • イベント/キューへ分離する: 同期呼び出しが不要なら、アウトボックスやキューで後続処理へ渡します。
  • テーブル駆動にする: 呼び出し先そのものではなく、処理対象や状態をテーブルに記録して別ジョブに処理させます。
  • 動的SQLは最終手段にする: コンパイル依存は切れますが、権限、検索性、テスト容易性が落ちます。
event-queue-decoupling.sql
CREATE TABLE order_event_queue (
  event_id    NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  event_type  VARCHAR2(50) NOT NULL,
  order_id    NUMBER NOT NULL,
  status      VARCHAR2(20) DEFAULT 'NEW' NOT NULL,
  payload     CLOB,
  created_at  TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
  processed_at TIMESTAMP
);

CREATE INDEX ix_order_event_queue_status
  ON order_event_queue(status, created_at);

-- 注文APIは請求パッケージを直接呼ばず、イベントを登録する
INSERT INTO order_event_queue(event_type, order_id, payload)
VALUES('ORDER_CREATED', :order_id, :payload_json);

ジョブやキューで分離する場合は、実行履歴や異常検知も必要になります。バッチ連携や非同期処理の監視は ジョブ実行履歴の集中管理と異常検知 と相性が良いです。

動的SQLで依存を切る場合の注意

EXECUTE IMMEDIATE は依存を切る手段にはなりますが、第一候補にしないほうが安全です。静的依存が消える代わりに、コンパイル時チェック、検索性、依存可視化、権限確認、テスト容易性が弱くなります。

dynamic-call-last-resort.sql
DECLARE
  v_package_name VARCHAR2(128) := 'PKG_BILLING_API';
  v_proc_name    VARCHAR2(128) := 'PROCESS_PAYMENT';
BEGIN
  -- 動的呼び出しを使うなら、呼び出し先をホワイトリスト化する。
  IF v_package_name NOT IN ('PKG_BILLING_API') THEN
    RAISE_APPLICATION_ERROR(-20000, 'Invalid dynamic target');
  END IF;

  EXECUTE IMMEDIATE
    'BEGIN ' || DBMS_ASSERT.SIMPLE_SQL_NAME(v_package_name) ||
    '.' || DBMS_ASSERT.SIMPLE_SQL_NAME(v_proc_name) ||
    '(:1, :2); END;'
    USING :p_order_id, :p_amount;
END;
/

どうしても動的呼び出しを使うなら、呼び出し先をテーブルや定数でホワイトリスト化し、DBMS_ASSERT で識別子を検査し、監査ログを残します。ただし、これは設計上の依存を消すというより、コンパイル依存を隠す手法です。乱用すると、障害時に「どこから呼ばれているか」が追えなくなります。

権限境界とAUTHIDを設計に含める

モジュール分割では、呼び出し関係だけでなく権限境界も重要です。AUTHID DEFINER はパッケージ所有者の権限で実行し、AUTHID CURRENT_USER は呼び出しユーザーの権限で実行します。どちらを使うかで、直接GRANT、ロール、シノニム、本番権限の扱いが変わります。

API層外部公開入口として、権限境界を明確にします。
DAO層テーブルへの直接アクセスを集約し、GRANT対象を絞ります。
Common層権限に依存しない純粋処理を中心にします。

デプロイ前の依存チェック

大規模システムでは、デプロイ前に依存違反とINVALIDを機械的に確認します。人のレビューだけに頼ると、共通パッケージやシノニム経由の依存を見落とします。

pre-deploy-dependency-check.sql
-- 1. INVALIDの残存確認
SELECT owner, object_type, object_name, status
FROM all_objects
WHERE owner = UPPER(:app_owner)
  AND status = 'INVALID'
ORDER BY object_type, object_name;

-- 2. 外部スキーマ参照の確認
SELECT owner, name, referenced_owner, referenced_name, referenced_type
FROM all_dependencies
WHERE owner = UPPER(:app_owner)
  AND referenced_owner IS NOT NULL
  AND referenced_owner <> owner
ORDER BY referenced_owner, referenced_type, referenced_name;

-- 3. 仕様部変更の影響確認
SELECT owner, name, type, referenced_owner, referenced_name, referenced_type
FROM all_dependencies
WHERE referenced_owner = UPPER(:app_owner)
  AND referenced_name = UPPER(:changed_package_name)
ORDER BY owner, type, name;

仕様部の変更は、参照元の再コンパイルや再テストを広く必要にします。パッケージ本体だけで閉じる変更と、仕様部を変える変更は、レビューの重さを分けて扱うべきです。

運用で見るチェックリスト

  • API、Service、DAO、Common、Batchの責務が説明できるか
  • DAOからService、Commonから業務パッケージへの依存がないか
  • パッケージ仕様部に内部用関数を出しすぎていないか
  • 循環依存を動的SQLで隠していないか
  • AUTHIDと直接GRANTの方針が決まっているか
  • ALL_DEPENDENCIESで依存違反を定期監査しているか
  • 仕様部変更時の影響範囲を事前に出しているか
  • INVALID再コンパイルをデプロイ手順に含めているか

まとめ

大規模PL/SQLシステムのモジュール分割では、パッケージを増やすことよりも、依存方向を一貫させることが重要です。API層、Service層、DAO層、Common層の責務を分け、仕様部を公開契約として扱い、ALL_DEPENDENCIES で実際の依存を監査します。

循環依存は動的SQLで隠す前に、共通型、API境界、イベント/キュー、責務分割で解消できないか検討します。権限境界、INVALID再コンパイル、デプロイ前チェックまで含めて設計すれば、PL/SQLを単なるスクリプト集ではなく、長期保守できる業務システムの基盤として扱えるようになります。