【PL/SQL】パッケージ設計でコード管理と再利用性を極める|レイヤード分割・Repository・依存制御・命名規約・テスト容易性

【PL/SQL】パッケージを使ったコード管理と再利用性向上 PL/SQL

PL/SQLプロジェクトが大きくなると、プロシージャ・ファンクションが数百〜数千個に膨れ、「どこに何があるか分からない」「同じ処理が3カ所に重複」「修正の影響範囲が読めない」といった保守地獄に陥りがちです。この問題を構造的に解決する仕組みがパッケージ(PACKAGE)で、関連するロジックを1つの論理単位にまとめ、インターフェース(公開)と実装(隠蔽)を分離できます。

ただし「とりあえずパッケージにまとめる」だけでは効果は限定的で、レイヤード分割命名規約依存方向の制御状態管理の境界テスト容易性といったアーキテクチャ視点の設計を組み合わせて初めて、「何百本パッケージがあっても破綻しないシステム」が手に入ります。

この記事では基本構文の解説ではなく、実務で大規模PL/SQLを書くためのパッケージ設計術に絞って解説します。レイヤ分割の標準形、Repository/Service Facade/Singleton等のOO設計パターンをパッケージで実現する方法、依存方向と循環依存防止、パッケージステートの安全な使い所、命名規約、テスト容易性、リファクタリング、アンチパターン7選、FAQ10問まで2026年版で整理します。

この記事でわかること

  • SPECとBODY分離による「クライアントを縛らない」インターフェース設計
  • レイヤード分割(API/Service/Repository/Util)の標準形
  • Repositoryパターンでテーブルアクセスを集中管理する実装
  • Service Facadeでビジネス処理を整理しトランザクション境界を明確化
  • Singletonパターンで設定値・接続情報を共有する技法
  • 依存方向ルール(上位→下位のみ)と循環依存の検出・解消
  • パッケージステート(セッションスコープ変数)の安全な使い所と罠
  • 初期化セクションを活用した遅延初期化・キャッシュロード
  • 命名規約のテンプレート(接頭辞・接尾辞・大文字小文字)
  • テスト容易性を高める依存注入とモック化のパターン
  • 本番で踏むアンチパターン7選と修正パターン
スポンサーリンク
  1. 30秒でわかるパッケージ設計の結論
  2. SPECとBODYの設計原則|「契約」と「実装」を分離する
    1. SPECに置くべきもの
    2. BODYに隠蔽すべきもの
  3. レイヤード分割|パッケージの「住所」を一意に決める
    1. API層(pkg_api_*)|外部公開エンドポイント
    2. Service層(pkg_*_service)|ビジネスロジック
    3. Repository層(pkg_*_repo)|テーブルアクセス専用
    4. Util層(pkg_util_*)|横断的ユーティリティ
  4. 設計パターン|OOPのRepository/Facade/Singletonをパッケージで表現
    1. Repositoryパターン|テーブルアクセスを抽象化
    2. Service Facadeパターン|複雑な手続きを1つの窓口にまとめる
    3. Singletonパターン|共有設定とコネクション情報
  5. 依存方向の制御|循環依存を構造的に排除する
    1. 許される依存方向
    2. 循環依存の検出
  6. パッケージステートの安全な使い所と罠
    1. 使ってよいケース
    2. 使ってはいけないケース
  7. 命名規約とコーディングスタイル|プロジェクト初日に決めるべき事
    1. パッケージ名
    2. サブプログラム名
    3. 変数・定数
    4. 大文字小文字
  8. テスト容易性|パッケージで依存注入とモック化を実現
    1. パッケージ変数経由で依存を注入する
  9. 本番で踏むアンチパターン7選
    1. ① 巨大な「便利パッケージ」を1本作る
    2. ② SPECにロジックの詳細を書く
    3. ③ Repository層を介さずService層がSQLを書く
    4. ④ パッケージステートをコネクションプール環境で使う
    5. ⑤ 循環依存を「FORWARD宣言で凌ぐ」
    6. ⑥ 命名規約をプロジェクト後半に決める
    7. ⑦ ACCESSIBLE BYを使わず想定外の場所から呼ばれる
  10. よくある質問
  11. 関連記事で知識を深める
  12. まとめ|パッケージ設計でPL/SQLを保守可能なアーキテクチャに

30秒でわかるパッケージ設計の結論

忙しい読者向けの結論先出しです。

結論 理由・効果
SPECは契約・BODYは実装と明確に役割分離 クライアントはSPECだけ読めば使える。BODY変更で再コンパイル波及を最小化
レイヤード分割(API/Service/Repository/Util) コードの「居場所」が一意に決まり保守コスト激減
テーブルアクセスはRepository専用パッケージに集中 SQLの分散を防ぎ性能チューニングと変更を1箇所で完結
④ 依存方向は上位→下位の一方通行 循環依存はコンパイル順序の罠と境界曖昧化の温床
パッケージステート使用は最小限 セッション内で値が残るためバッチ/オンライン混在で事故
命名規約をプロジェクト初日に決める 後から統一は地獄。pkg_接頭辞・大文字定数等の徹底
⑦ ACCESSIBLE BYで許可呼び出し元を明示 「想定外の場所から呼ばれて困る」を構造的に防ぐ(12c+)

SPECとBODYの設計原則|「契約」と「実装」を分離する

パッケージを使う最大の利点は「呼び出し側にBODYの再コンパイルを波及させない」こと。これを実現するためにSPECは「呼び出し側に対する契約書」と捉え、実装の都合で変えないのが鉄則です。BODYを変更してもSPECが同じならクライアント側のINVALID化は起きません。

SPECに置くべきもの

外部に公開するインターフェース宣言のみ。PROCEDURE/FUNCTIONのシグネチャ、公開定数、公開型、公開例外。SPECにロジックは書きません(書けません)。コメントは「使う人が読むドキュメント」としてシグネチャの直上に必ず書きます。

BODYに隠蔽すべきもの

SPECで宣言した処理の実装と、内部だけで使うヘルパー関数・定数・変数。テーブル名・SQL文・例外の捕捉ロジックなどはすべてBODY側に隠蔽し、SPECからは推測できないようにします。

SPECは契約のみ・BODYは実装のみ
-- ✅ SPEC: 契約だけ(クライアントが読むドキュメント)
CREATE OR REPLACE PACKAGE pkg_order_service AS
  /*
   * 注文関連のビジネスロジック層
   * 利用例:
   *   v_order_id := pkg_order_service.create_order(p_customer_id, p_items);
   */

  -- 公開例外
  e_invalid_customer EXCEPTION;
  PRAGMA EXCEPTION_INIT(e_invalid_customer, -20101);

  -- 注文作成(複数明細を一括登録、IDを返す)
  FUNCTION create_order(
    p_customer_id IN NUMBER,
    p_items       IN order_item_tab    -- 公開型(別パッケージで定義)
  ) RETURN NUMBER;

  -- 注文取消
  PROCEDURE cancel_order(p_order_id IN NUMBER);

  -- 注文金額の取得
  FUNCTION calc_total(p_order_id IN NUMBER) RETURN NUMBER;
END pkg_order_service;
/

-- ✅ BODY: 実装と内部詳細を完全隠蔽
CREATE OR REPLACE PACKAGE BODY pkg_order_service AS

  -- 内部定数(SPECに書く必要はない)
  c_status_active CONSTANT VARCHAR2(10) := 'ACTIVE';

  -- 内部ヘルパー(外部から呼ばれない)
  FUNCTION resolve_tax_rate(p_country VARCHAR2) RETURN NUMBER IS
  BEGIN
    RETURN CASE p_country WHEN 'JP' THEN 0.10 ELSE 0.00 END;
  END;

  FUNCTION create_order(
    p_customer_id IN NUMBER,
    p_items       IN order_item_tab
  ) RETURN NUMBER AS
    v_order_id NUMBER;
  BEGIN
    -- Repositoryに移譲(SQLは書かない)
    v_order_id := pkg_order_repo.insert_order(p_customer_id);
    pkg_order_repo.insert_items(v_order_id, p_items);
    RETURN v_order_id;
  END;

  PROCEDURE cancel_order(p_order_id IN NUMBER) AS
  BEGIN
    pkg_order_repo.update_status(p_order_id, 'CANCELLED');
  END;

  FUNCTION calc_total(p_order_id IN NUMBER) RETURN NUMBER AS
  BEGIN
    RETURN pkg_order_repo.sum_amount(p_order_id);
  END;
END pkg_order_service;
/

SPECに「変えると痛い」ものは何でも書かないでください。たとえばRECORD型のフィールド構成をSPECに公開すると、フィールド追加で型サイズが変わりクライアントを再コンパイルさせる必要が出ます。「シグネチャを安定させる」「型は最低限の公開」「内部詳細は隠す」を徹底してください。

レイヤード分割|パッケージの「住所」を一意に決める

大規模システムでは「処理がどこに書かれているか」が分散すると保守不能になります。パッケージを役割別レイヤに分けてしまうのが最も効果的な対策です。4層構造が標準形で、各レイヤは下位レイヤだけを呼び出せるルールを徹底します。

API層(pkg_api_*)|外部公開エンドポイント

WebアプリやAPIサーバから直接呼ばれる入口。引数バリデーション・トランザクション境界・例外の翻訳を担当し、ビジネスロジックは書かずService層へ委譲します。入力サニタイズ・出力フォーマット変換・監査ログ書き込みもこのレイヤです。

Service層(pkg_*_service)|ビジネスロジック

業務ルール・複数テーブルにまたがる処理を集約。直接SQLを書かず、Repository層を呼び出して必要なデータを取得・更新します。純粋にビジネスルールだけが書かれている状態を維持するとコードレビューも単体テストも格段にやりやすくなります。

Repository層(pkg_*_repo)|テーブルアクセス専用

テーブルごとのCRUD操作・検索SQLを集約。このレイヤだけがSQL文を書くのがルール。同じテーブルへのアクセスがあちこちに散らばる事態を防ぎます。索引追加・パフォーマンスチューニング・スキーマ変更の影響範囲がこのパッケージ1本に閉じ込められ、変更コストが激減します。

Util層(pkg_util_*)|横断的ユーティリティ

日付処理・文字列処理・暗号化・ロギングなど業務ドメインに依存しない汎用処理。上位レイヤから自由に呼び出せますが、Util層から業務パッケージを呼ぶのは厳禁(依存が逆流する)。

レイヤード分割の最小実装例
-- ┌─────────────┐
-- │ API層       │ pkg_api_order
-- ├─────────────┤
-- │ Service層   │ pkg_order_service
-- ├─────────────┤
-- │ Repository層│ pkg_order_repo
-- ├─────────────┤
-- │ Util層      │ pkg_util_date / pkg_util_log
-- └─────────────┘

-- API層: 外部入口(バリデーション・例外翻訳・監査ログ)
CREATE OR REPLACE PACKAGE pkg_api_order AS
  FUNCTION post_order(p_json CLOB) RETURN CLOB;
END pkg_api_order;
/

CREATE OR REPLACE PACKAGE BODY pkg_api_order AS
  FUNCTION post_order(p_json CLOB) RETURN CLOB AS
    v_cust  NUMBER;
    v_items order_item_tab;
    v_id    NUMBER;
  BEGIN
    -- バリデーションとパース(Util層を活用)
    pkg_util_log.audit('post_order', p_json);
    pkg_util_json.parse_order(p_json, v_cust, v_items);

    -- ビジネス処理はService層に委譲
    v_id := pkg_order_service.create_order(v_cust, v_items);

    -- 結果フォーマット
    RETURN pkg_util_json.build_response(v_id);
  EXCEPTION
    WHEN pkg_order_service.e_invalid_customer THEN
      RETURN pkg_util_json.error_response(400, 'Invalid customer');
  END;
END pkg_api_order;
/

-- Repository層: SQLはここだけに集中
CREATE OR REPLACE PACKAGE pkg_order_repo AS
  FUNCTION insert_order(p_customer_id NUMBER) RETURN NUMBER;
  PROCEDURE insert_items(p_order_id NUMBER, p_items order_item_tab);
  PROCEDURE update_status(p_order_id NUMBER, p_status VARCHAR2);
  FUNCTION sum_amount(p_order_id NUMBER) RETURN NUMBER;
END pkg_order_repo;
/

CREATE OR REPLACE PACKAGE BODY pkg_order_repo AS
  FUNCTION insert_order(p_customer_id NUMBER) RETURN NUMBER AS
    v_id NUMBER;
  BEGIN
    INSERT INTO orders(order_id, customer_id, status, created_at)
    VALUES(orders_seq.NEXTVAL, p_customer_id, 'NEW', SYSDATE)
    RETURNING order_id INTO v_id;
    RETURN v_id;
  END;
  -- 他の実装は省略
END pkg_order_repo;
/

設計パターン|OOPのRepository/Facade/Singletonをパッケージで表現

パッケージは他言語の「クラス」に近い構造を持つので、OOPの設計パターンをそのまま応用できます。実務で頻出する3パターンを紹介します。

Repositoryパターン|テーブルアクセスを抽象化

前述のRepository層がまさにこれ。テーブル単位で1パッケージを作り、CRUD操作を公開します。呼び出し側はSQLを意識せずビジネスロジックに集中でき、テーブル分割・JOIN方法変更・索引チューニングの影響範囲がパッケージ内部に閉じ込められます。

Service Facadeパターン|複雑な手続きを1つの窓口にまとめる

「注文確定」のように内部で5つのテーブル更新と外部API呼び出しが必要な処理は、Service Facadeとして1つの公開関数に隠蔽します。クライアントはpkg_order_service.checkout(p_id)を呼ぶだけで内部の複雑さを意識しません。トランザクション境界もこの関数内で完結させるのが定石です。

Singletonパターン|共有設定とコネクション情報

パッケージ変数はセッション内で1インスタンスというSingletonの性質を最初から持っています。設定値・接続情報・キャッシュなどセッション全体で1つだけ持ちたい状態に最適。ただしセッションをまたぐ共有はできないので、全プロセスで共有したい設定はテーブルに置くかSYS_CONTEXTを使います。

Singletonパターン|設定キャッシュを初期化セクションでロード
CREATE OR REPLACE PACKAGE pkg_config AS
  FUNCTION get(p_key VARCHAR2) RETURN VARCHAR2;
  PROCEDURE refresh;
END pkg_config;
/

CREATE OR REPLACE PACKAGE BODY pkg_config AS
  -- セッション内で1つだけ存在する状態
  TYPE t_map IS TABLE OF VARCHAR2(4000) INDEX BY VARCHAR2(64);
  g_cache  t_map;

  PROCEDURE refresh AS
  BEGIN
    g_cache.DELETE;   -- 全クリア
    FOR rec IN (SELECT k, v FROM app_config) LOOP
      g_cache(rec.k) := rec.v;
    END LOOP;
  END;

  FUNCTION get(p_key VARCHAR2) RETURN VARCHAR2 AS
  BEGIN
    RETURN g_cache(p_key);
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      RETURN NULL;
  END;
BEGIN
  -- 初期化セクション: パッケージが初参照されたときに1度だけ実行
  refresh;
END pkg_config;
/

-- 利用例(高速・SQL不要)
DECLARE
  v_url VARCHAR2(200);
BEGIN
  v_url := pkg_config.get('api.endpoint');
END;
/

Singletonの落とし穴:パッケージ変数の値はセッション持続中ずっと残るので、①コネクションプール経由で別ユーザがセッションを使い回すと値が混ざる、②設定変更を即時反映するにはrefreshを明示的に呼ぶ必要がある、という2点に注意してください。本番では「設定変更時にrefreshを全セッションに通知する」仕組み(イベント駆動・テーブルバージョン番号比較)が必要になります。

依存方向の制御|循環依存を構造的に排除する

パッケージAがBを呼び、BがAを呼ぶ「循環依存」はPL/SQLではコンパイル順序の罠になります。OracleはFORWARD宣言で凌げますが、設計として循環依存があるのはレイヤ境界が曖昧化している兆候。構造的に予防するルールが「上位レイヤ→下位レイヤの一方通行」です。

許される依存方向

API層→Service層→Repository層→Util層、という上から下への一方通行のみ許可します。逆方向(例: Repository層からService層を呼ぶ)は禁止。同じレイヤ間の依存も最小限に抑え、必要ならば共通機能をUtil層へ抽出します。

循環依存の検出

USER_DEPENDENCIESビューを使えばパッケージ間の依存グラフを取得できます。コンパイル時に検出すれば早期に気付けます。

循環依存・逆方向依存の検出クエリ
-- 全依存関係を一覧(自スキーマのパッケージのみ)
SELECT name, referenced_name, referenced_type
  FROM user_dependencies
 WHERE type = 'PACKAGE BODY'
   AND referenced_type = 'PACKAGE'
 ORDER BY name, referenced_name;

-- 循環依存を検出(Aが参照するBがAを参照している)
SELECT d1.name AS pkg_a, d1.referenced_name AS pkg_b
  FROM user_dependencies d1
  JOIN user_dependencies d2
    ON d1.name = d2.referenced_name
   AND d1.referenced_name = d2.name
 WHERE d1.type = 'PACKAGE BODY'
   AND d2.type = 'PACKAGE BODY'
   AND d1.referenced_type = 'PACKAGE'
   AND d2.referenced_type = 'PACKAGE';

-- レイヤ違反を検出(命名規約 pkg_*_repo が pkg_*_service を呼んでいないか)
SELECT name, referenced_name
  FROM user_dependencies
 WHERE name LIKE 'PKG_%_REPO'
   AND referenced_name LIKE 'PKG_%_SERVICE'
   AND type = 'PACKAGE BODY';

同レイヤ内の依存(例: pkg_order_serviceがpkg_inventory_serviceを呼ぶ)はやむを得ず発生しますが、双方向にならないよう注意。一方向のみ呼ぶようリファクタリングするか、共通処理を別レイヤ(Util層)に抽出して両者からそこを呼ぶ形にしてください。

パッケージステートの安全な使い所と罠

パッケージ変数(BODY内のグローバル変数)はセッション持続中ずっと値が残るという強力な特性を持ちますが、これが諸刃の剣です。正しく使えばキャッシュ・設定保持に極めて有効、間違って使うとセッション間で値が混ざる原因になります。

使ってよいケース

  • セッション固有の設定キャッシュ(変更頻度の低い設定値)
  • クッキーセッションのようなオンライン処理での一時状態保持
  • 大量のBULK COLLECT結果の使い回し(同一セッション・同一処理内)
  • 初期化コストが大きいオブジェクトの遅延ロード

使ってはいけないケース

  • コネクションプール経由で別ユーザがセッションを使い回す環境(前ユーザの値が残って情報漏洩)
  • マルチセッション間で共有したい状態(共有不能、別セッションでは別値)
  • 処理間で「必ずクリアされている」前提の値(前回処理の残存値で誤動作)
  • 外部APIのレスポンス(NULL初期化忘れで前のレスポンスが見える)

「コネクションプール+パッケージステート」は重大な事故源です。JavaのコネクションプールやAPサーバから来るセッションは、ユーザAが使い終わった後にユーザBが同じセッションを再利用することが普通にあります。このとき、ユーザAが書き込んだパッケージ変数がユーザBから見える、という情報漏洩・認可バイパスに直結します。コネクションプール環境ではDBMS_SESSION.MODIFY_PACKAGE_STATEでセッション返却時に明示的に状態を消去するか、そもそもパッケージステートに依存しない設計に変えてください。

コネクションプールでパッケージステートを安全に扱う
-- 設定キャッシュ
CREATE OR REPLACE PACKAGE pkg_user_ctx AS
  PROCEDURE set_current_user(p_user_id NUMBER);
  FUNCTION  get_current_user RETURN NUMBER;
  PROCEDURE clear;
END;
/

CREATE OR REPLACE PACKAGE BODY pkg_user_ctx AS
  g_user_id NUMBER;
  PROCEDURE set_current_user(p_user_id NUMBER) IS
  BEGIN g_user_id := p_user_id; END;
  FUNCTION get_current_user RETURN NUMBER IS BEGIN RETURN g_user_id; END;
  PROCEDURE clear IS BEGIN g_user_id := NULL; END;
END;
/

-- アプリケーション側(Java/.NET等)でセッション返却時に必ずクリア
-- BEGIN pkg_user_ctx.clear; END;
-- もしくは、全パッケージステートを一括リセット
-- BEGIN DBMS_SESSION.RESET_PACKAGE; END;

命名規約とコーディングスタイル|プロジェクト初日に決めるべき事

大規模PL/SQLは命名規約の徹底だけで保守性が劇的に改善します。後から統一するのは現実的に不可能なので、プロジェクト初日に決めて全員に守らせてください。

パッケージ名

  • すべてPKG_で始める(テーブルとビュー名と区別するため)
  • レイヤを示す接尾辞:_API_SERVICE_REPO_UTIL
  • 例: PKG_ORDER_SERVICEPKG_INVENTORY_REPOPKG_UTIL_DATE
  • 30文字制限を意識し短く(11g以前は30文字、12.2以降は128文字)

サブプログラム名

  • 動詞始まり:create_orderfind_by_idcalc_total
  • Boolean返却関数はis_has_can_で始める
  • 同じ動詞は同じ意味で統一(getとfindの混在を避ける)

変数・定数

  • パラメータ:p_接頭辞(p_customer_id
  • ローカル変数:v_接頭辞(v_order_id
  • グローバル変数:g_接頭辞(g_cache
  • 定数:c_接頭辞・全大文字(c_MAX_RETRY
  • カーソル:cur_接頭辞
  • 例外:e_接頭辞(e_invalid_state

大文字小文字

  • キーワード:大文字(BEGINEXCEPTION
  • 識別子:小文字(v_countcreate_order
  • 定数値(マジックナンバー禁止):c_定数で命名

テスト容易性|パッケージで依存注入とモック化を実現

「PL/SQLは単体テストしにくい」と言われがちですが、レイヤ分割を徹底すれば純粋なService層は単体テスト可能です。鍵は「Service層がRepository層をどう参照するか」を差し替え可能にする工夫です。

パッケージ変数経由で依存を注入する

Service層からRepositoryを直接呼ばず、関数ポインタ的なRepository「実装」をパッケージ変数で持ち、テスト時はモックパッケージに差し替えるパターンです。Oracle 12c以降の「ファンクションポインタ」(PROCEDURE型)が使えます。

モック差し替え可能なService層実装
-- Repositoryインターフェースをパッケージ仕様で抽象化
CREATE OR REPLACE PACKAGE pkg_order_repo_iface AS
  FUNCTION insert_order(p_cust NUMBER) RETURN NUMBER;
END;
/

-- 本番実装
CREATE OR REPLACE PACKAGE BODY pkg_order_repo_iface AS
  FUNCTION insert_order(p_cust NUMBER) RETURN NUMBER AS
  BEGIN
    RETURN pkg_order_repo.insert_order(p_cust);
  END;
END;
/

-- Service層は「ifaceを呼ぶ」と決まっている
CREATE OR REPLACE PACKAGE BODY pkg_order_service AS
  FUNCTION create_order(p_cust NUMBER) RETURN NUMBER AS
  BEGIN
    -- 本番でもテストでも同じコード
    RETURN pkg_order_repo_iface.insert_order(p_cust);
  END;
END;
/

-- テスト時はpkg_order_repo_ifaceのBODYだけ差し替えてモック化
-- CREATE OR REPLACE PACKAGE BODY pkg_order_repo_iface AS
--   FUNCTION insert_order(p_cust NUMBER) RETURN NUMBER AS
--   BEGIN
--     RETURN 99999;  -- モック値を返すだけ
--   END;
-- END;
-- /

-- テスト関数(utPLSQLフレームワーク等)
CREATE OR REPLACE PROCEDURE test_create_order AS
BEGIN
  ASSERT(pkg_order_service.create_order(100) = 99999, 'モックが返るはず');
END;
/

PL/SQL専用のテストフレームワークutPLSQLgithub.com/utPLSQL/utPLSQL)を使うと、パッケージ単位でテスト宣言・モック化・カバレッジ計測まで可能です。CI/CDに組み込めば「リリース前に全パッケージのテストを通す」運用が現実になります。ピュアなRepository層のテストはDB前提の統合テスト、Service層は依存注入でユニットテスト、と切り分けるのが実務の王道です。

本番で踏むアンチパターン7選

① 巨大な「便利パッケージ」を1本作る

「pkg_common」「pkg_util」と称して数千行のパッケージにあらゆる処理を詰め込むと、責務不明・テスト不能・変更の影響範囲爆発で破綻します。1パッケージは1責務を徹底し、500〜1500行程度を上限の目安にしてください。

② SPECにロジックの詳細を書く

RECORDフィールドを過剰に公開する、内部用定数までSPECに置くなど、「BODY変更時にSPECも変えざるを得ない」状況を生みます。結果クライアントの再コンパイルが頻発し、SPEC隠蔽の意義が消滅します。SPECは「これだけ知っていれば使える」最小に絞ってください。

③ Repository層を介さずService層がSQLを書く

同じテーブルへのSELECTが10カ所に散らばり、索引追加やテーブル分割の影響範囲が読めなくなります。SQLを書くのはRepository層だけを徹底すれば変更コストが激減します。

④ パッケージステートをコネクションプール環境で使う

セッション再利用で前ユーザの値が見える事故が起きます。コネクションプール経由なら状態を持たない関数として書くか、セッション返却時に必ずクリアする運用を組んでください。

⑤ 循環依存を「FORWARD宣言で凌ぐ」

FORWARD宣言は同一パッケージ内の前方参照のためのもので、パッケージ間の循環依存解決には適しません。循環がある=レイヤ設計の失敗と認識して共通処理を抽出するリファクタリングを優先してください。

⑥ 命名規約をプロジェクト後半に決める

500本のパッケージが乱雑な名前で乱立してから統一しようとすると、コンパイル順序・他システム連携・既存呼び出し元の修正で挫折します。初日に決めて100%守ることが唯一の解決策です。

⑦ ACCESSIBLE BYを使わず想定外の場所から呼ばれる

本来内部ユーティリティのつもりが他モジュールから直接呼ばれ、変更不能になる事故。Oracle 12c以降のACCESSIBLE BY句で「許可された呼び出し元」を明示すれば構造的に防げます。内部実装はすべてACCESSIBLE BY (PACKAGE pkg_x)などで呼び出し元を絞り込むのが定石です。

よくある質問

Q1パッケージは何行が適切?
A目安は500〜1500行。500行未満だと粒度が細かすぎて関連性のあるロジックが分散、1500行を超えると認知負荷が上がり責務が曖昧になります。責務(テーブル単位・業務ドメイン単位)で1パッケージにまとめ、行数が膨らんできたら「pkg_order_service」を「pkg_order_create_service」「pkg_order_cancel_service」のようにユースケースで分割するのが定石です。
Qパッケージとプロシージャ単体、どちらを使うべき?
A原則パッケージ一択です。プロシージャ・ファンクション単体(standalone)は①依存追跡が困難、②命名空間がスキーマ直下に増える、③再コンパイル時に粒度が粗い、というデメリットがあり大規模システムでは負債になります。「とりあえずpkg_misc」のような器を1本用意して、行き場のない関数はそこに集約するだけでも保守性が大幅に向上します。
Qパッケージ変数を使うとセッション間で値が共有されますか?
Aいいえ、セッションごとに完全に独立しています。セッション内では値が残りますが、別セッションからは見えません。全プロセス共有が必要な値はSYS_CONTEXTUSERENV)/グローバルテンポラリ表/キャッシュ表(RESULT_CACHE)/設定テーブルなどを使います。
Qパッケージの初期化セクションはいつ実行されますか?
Aセッション内でそのパッケージが初めて参照された時点で1度だけ実行されます。セッションが続く限り再実行されないので、キャッシュロード・接続情報初期化など「重い初期化を1回だけやりたい処理」に最適です。注意点として、初期化処理が失敗するとパッケージ全体が使えなくなりORA-04068などが頻発するので、初期化セクションでは例外を慎重に扱ってください。
QACCESSIBLE BYを使うべき場面は?
A内部実装パッケージ(pkg_*_internal等)に必ず付けてください。ACCESSIBLE BY (PACKAGE pkg_order_service)と書くと指定したパッケージ以外からの呼び出しがコンパイル時にエラーになります。実装の詳細を意図せず使われて変更不能になる事故を構造的に防げます。Oracle 12cの目玉機能の一つで、新規プロジェクトでは積極活用すべきです。
QPRAGMA SERIALLY_REUSABLEとは?
Aパッケージステートを「呼び出し終了時に毎回破棄する」モードに変えるプラグマです。CREATE PACKAGE pkg_x ... PRAGMA SERIALLY_REUSABLE;と書きます。コネクションプール環境でステート漏洩を防ぐ用途に向きますが、初期化セクションも毎回走るため重い初期化処理がある場合は性能劣化に注意。本番でステート保持が問題になる場合の現実解の一つです。
QutPLSQLは無料で使えますか?
A完全無料・オープンソース(Apache 2.0)です。GitHubのutPLSQL/utPLSQLからインストールでき、JUnit風のテストアノテーション・カバレッジ測定・CI連携を提供します。商用Oracle環境にも導入可能で、本格的なTDDを実践したいPL/SQLチームの定番ツールです。
Qパッケージ間の依存をビジュアル化したい
AUSER_DEPENDENCIESビューをCSVに出してGraphvizやplantumlに流し込むのが定番です。たとえばSELECT name AS "src", referenced_name AS "dst" FROM user_dependencies WHERE type='PACKAGE BODY' AND referenced_type='PACKAGE';をCSV出力しPython等でDOT形式に変換すれば依存グラフが描けます。SQL Developerの「依存性ビューア」も簡易確認には便利です。
Q巨大なレガシーパッケージを段階的にリファクタリングするには?
AStrangler Fig(絞め殺し)パターンを推奨します。①新しいレイヤード構成のパッケージ(pkg_v2_*)を別名で作る、②旧パッケージ内の機能を1つずつ新パッケージに移植、③旧パッケージはラッパーとして新パッケージを呼ぶだけに変える、④全クライアントが新パッケージを直接呼ぶよう移行、⑤旧パッケージを削除。一気に書き換えず段階的に置換することで業務影響を最小化できます。
Qパッケージのバージョニングはどう管理する?
ASPECにバージョン定数を持たせてc_VERSION CONSTANT VARCHAR2(10) := '2.3.1';のように記録、本番デプロイ時に確認するのが手軽な手法です。本格的にやるならGit管理+デプロイ時にバージョン番号を埋めるビルドステップを組み込みます。メジャーバージョンを上げるときは旧パッケージをpkg_x_v1として残しクライアントの移行猶予期間を作るとリスクが下がります。

関連記事で知識を深める

パッケージ設計と関連する周辺技術もあわせて押さえておきましょう。

まとめ|パッケージ設計でPL/SQLを保守可能なアーキテクチャに

パッケージは「関連処理をまとめる入れ物」ではなく、大規模システムの設計を支えるアーキテクチャ要素です。SPECとBODYの分離、レイヤード分割、設計パターンの適用、依存方向の制御、命名規約の徹底、テスト容易性の確保、これらを組み合わせて初めて「数百本パッケージがあっても破綻しない」システムが手に入ります。本記事の要点を7つに集約します。

  1. SPECは「契約」・BODYは「実装」と明確に役割分離
  2. レイヤード分割(API/Service/Repository/Util)を初日から徹底
  3. SQLはRepository層だけ、ビジネスロジックはService層だけ
  4. 依存方向は上位→下位の一方通行、循環依存はリファクタリングで解消
  5. パッケージステートはコネクションプール環境で慎重運用、原則は無状態
  6. 命名規約(pkg_接頭辞・レイヤ接尾辞・変数接頭辞)を全員で守る
  7. ACCESSIBLE BY+utPLSQLでカプセル化とテストを構造化する

レガシーコードでこれらが守られていない場合は、まずRepository層を作って既存SQLを集約することから始めてください。たったそれだけで「テーブル変更時にどこを直せばよいか」が読めるようになり、保守速度が劇的に変わります。本記事を実装テンプレとして自プロジェクトの構造を見直してみてください。