【PL/SQL】設定値管理テーブルとFeature Flag設計|環境差分・即時切替・監査ログまで

【PL/SQL】設定値管理テーブルとFeature Flag設計|環境差分・即時切替・監査ログまで PL/SQL

PL/SQLの業務処理では、締め時刻、外部連携のON/OFF、リトライ回数、検証用の動作モードなどをコードに直接書いてしまいがちです。しかし、設定値をハードコードすると、本番だけ値を変えたい、障害時に一時停止したい、段階リリースしたい、といった運用に弱くなります。設定値管理テーブルとFeature Flagを用意しておくと、デプロイなしで動作を切り替えられます。

この記事では、PL/SQLから安全に参照できる設定値テーブル、Feature Flag判定パッケージ、変更監査ログ、即時反映とキャッシュの考え方、権限設計までを実務向けに整理します。パッケージ分割の考え方は PL/SQLパッケージ設計、テストで設定を差し替える考え方は テスト容易性を高めるプロシージャ設計 と相性がよいです。

この記事で扱うこと

  • 設定値管理テーブルとFeature Flagテーブルの分け方
  • 環境別、機能別、期間別に設定を持つ方法
  • PL/SQLパッケージから型安全に値を取得する方法
  • Feature FlagのON/OFF判定と段階リリース
  • 変更履歴、監査ログ、権限、キャッシュの注意点
スポンサーリンク

設定値管理で解決したい問題

設定値管理の目的は、コード変更と運用変更を分離することです。たとえば、外部API連携を一時停止する、夜間バッチの対象件数を抑える、新機能を一部ユーザーだけ有効にする、といった判断は、デプロイではなく運用操作で切り替えたい場面があります。

  • 本番、検証、開発で値を変えたい
  • 障害時に外部連携や重い処理を止めたい
  • 新機能を小さく有効化して様子を見たい
  • 誰がいつ設定を変えたか追跡したい
  • SQLやPL/SQLの中にマジックナンバーを残したくない

ただし、何でも設定化すると逆に危険です。業務仕様として固定すべき値、権限に関わる値、秘密情報は、安易に一般設定テーブルへ置かない方が安全です。

設定値テーブルの基本設計

まずは、環境、キー、値、型、有効期間、更新者を持つ設定値テーブルを用意します。値は文字列として保存し、PL/SQLパッケージ側で型変換する方が、設定の追加に強くなります。

app-config-ddl.sql
CREATE TABLE app_config (
  env_name       VARCHAR2(30)  NOT NULL,
  config_key     VARCHAR2(100) NOT NULL,
  value_type     VARCHAR2(20)  NOT NULL,
  value_text     VARCHAR2(4000),
  enabled        CHAR(1) DEFAULT 'Y' NOT NULL,
  effective_from TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
  effective_to   TIMESTAMP,
  description    VARCHAR2(1000),
  updated_by     VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV','SESSION_USER') NOT NULL,
  updated_at     TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
  CONSTRAINT pk_app_config PRIMARY KEY (env_name, config_key, effective_from),
  CONSTRAINT ck_app_config_type CHECK (value_type IN ('VARCHAR2','NUMBER','BOOLEAN','JSON')),
  CONSTRAINT ck_app_config_enabled CHECK (enabled IN ('Y','N'))
);

CREATE INDEX ix_app_config_current
  ON app_config(env_name, config_key, enabled, effective_to);

effective_from を主キーに含めると、同じキーの変更履歴を残しやすくなります。一方、常に最新値だけを更新する設計にしたい場合は、履歴を別テーブルに分ける方が扱いやすいです。

設定値取得パッケージを用意する

アプリケーションコードから設定テーブルを直接SELECTさせると、型変換やデフォルト値の扱いが散らばります。必ず専用パッケージを作り、get_varchar2get_numberget_boolean のように型ごとの関数を公開します。

app-config-package.sql
CREATE OR REPLACE PACKAGE app_config_pkg AS
  FUNCTION get_varchar2(
    p_key     VARCHAR2,
    p_default VARCHAR2 DEFAULT NULL,
    p_env     VARCHAR2 DEFAULT NULL
  ) RETURN VARCHAR2;

  FUNCTION get_number(
    p_key     VARCHAR2,
    p_default NUMBER DEFAULT NULL,
    p_env     VARCHAR2 DEFAULT NULL
  ) RETURN NUMBER;

  FUNCTION get_boolean(
    p_key     VARCHAR2,
    p_default BOOLEAN DEFAULT FALSE,
    p_env     VARCHAR2 DEFAULT NULL
  ) RETURN BOOLEAN;
END;
/

CREATE OR REPLACE PACKAGE BODY app_config_pkg AS
  FUNCTION current_env RETURN VARCHAR2 IS
  BEGIN
    RETURN COALESCE(
      SYS_CONTEXT('APP_CTX', 'ENV_NAME'),
      SYS_CONTEXT('USERENV', 'DB_NAME')
    );
  END;

  FUNCTION raw_value(p_key VARCHAR2, p_env VARCHAR2) RETURN VARCHAR2 IS
    v_value app_config.value_text%TYPE;
    v_env   app_config.env_name%TYPE;
  BEGIN
    v_env := COALESCE(p_env, current_env);

    SELECT value_text
      INTO v_value
      FROM app_config
     WHERE env_name = v_env
       AND config_key = p_key
       AND enabled = 'Y'
       AND effective_from <= SYSTIMESTAMP
       AND (effective_to IS NULL OR effective_to > SYSTIMESTAMP)
     ORDER BY effective_from DESC
     FETCH FIRST 1 ROW ONLY;

    RETURN v_value;
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      RETURN NULL;
  END;

  FUNCTION get_varchar2(
    p_key     VARCHAR2,
    p_default VARCHAR2,
    p_env     VARCHAR2
  ) RETURN VARCHAR2 IS
  BEGIN
    RETURN COALESCE(raw_value(p_key, p_env), p_default);
  END;

  FUNCTION get_number(
    p_key     VARCHAR2,
    p_default NUMBER,
    p_env     VARCHAR2
  ) RETURN NUMBER IS
    v_raw VARCHAR2(4000);
  BEGIN
    v_raw := raw_value(p_key, p_env);

    IF v_raw IS NULL THEN
      RETURN p_default;
    END IF;

    RETURN TO_NUMBER(v_raw);
  EXCEPTION
    WHEN VALUE_ERROR OR INVALID_NUMBER THEN
      RAISE_APPLICATION_ERROR(-20010, 'invalid number config: ' || p_key);
  END;

  FUNCTION get_boolean(
    p_key     VARCHAR2,
    p_default BOOLEAN,
    p_env     VARCHAR2
  ) RETURN BOOLEAN IS
    v_raw VARCHAR2(4000);
  BEGIN
    v_raw := UPPER(raw_value(p_key, p_env));

    IF v_raw IS NULL THEN
      RETURN p_default;
    ELSIF v_raw IN ('Y','YES','TRUE','1','ON') THEN
      RETURN TRUE;
    ELSIF v_raw IN ('N','NO','FALSE','0','OFF') THEN
      RETURN FALSE;
    ELSE
      RAISE_APPLICATION_ERROR(-20011, 'invalid boolean config: ' || p_key);
    END IF;
  END;
END;
/

設定値の型変換失敗を握りつぶすと、本番で意図しない動作になります。数値やBOOLEANの変換に失敗した場合は例外にし、どのキーが不正か分かるメッセージを返す方が安全です。例外の分類は PL/SQLの例外設計と再試行パターン が参考になります。

Feature Flagテーブルを分ける

単純な設定値とFeature Flagは似ていますが、運用上は分けた方が管理しやすいです。Feature Flagは、機能名、対象環境、有効/無効、段階リリース率、説明、期限を持たせます。期限を持たせることで、リリース後に不要になったフラグを放置しにくくなります。

feature-flag-ddl.sql
CREATE TABLE feature_flag (
  flag_key       VARCHAR2(100) NOT NULL,
  env_name       VARCHAR2(30)  NOT NULL,
  enabled        CHAR(1) DEFAULT 'N' NOT NULL,
  rollout_pct    NUMBER(3) DEFAULT 0 NOT NULL,
  expires_at     TIMESTAMP,
  description    VARCHAR2(1000),
  updated_by     VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV','SESSION_USER') NOT NULL,
  updated_at     TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
  CONSTRAINT pk_feature_flag PRIMARY KEY (flag_key, env_name),
  CONSTRAINT ck_feature_flag_enabled CHECK (enabled IN ('Y','N')),
  CONSTRAINT ck_feature_flag_pct CHECK (rollout_pct BETWEEN 0 AND 100)
);

enabled は全体のON/OFF、rollout_pct は段階リリース率です。全員OFFなら enabled='N'、一部だけONなら enabled='Y' かつ rollout_pct を小さくします。

Feature Flag判定関数を作る

フラグ判定も直接SELECTせず、専用パッケージに閉じ込めます。ユーザーIDや取引先IDなど、安定したキーからハッシュ値を作り、ロールアウト率以下なら有効にする設計にすると、同じユーザーで結果が揺れません。

feature-flag-package.sql
CREATE OR REPLACE PACKAGE feature_flag_pkg AS
  FUNCTION is_enabled(
    p_flag_key VARCHAR2,
    p_subject  VARCHAR2 DEFAULT NULL,
    p_env      VARCHAR2 DEFAULT NULL
  ) RETURN BOOLEAN;
END;
/

CREATE OR REPLACE PACKAGE BODY feature_flag_pkg AS
  FUNCTION current_env RETURN VARCHAR2 IS
  BEGIN
    RETURN COALESCE(
      SYS_CONTEXT('APP_CTX', 'ENV_NAME'),
      SYS_CONTEXT('USERENV', 'DB_NAME')
    );
  END;

  FUNCTION bucket(p_value VARCHAR2) RETURN NUMBER IS
  BEGIN
    RETURN ORA_HASH(p_value, 99);
  END;

  FUNCTION is_enabled(
    p_flag_key VARCHAR2,
    p_subject  VARCHAR2,
    p_env      VARCHAR2
  ) RETURN BOOLEAN IS
    v_enabled feature_flag.enabled%TYPE;
    v_pct     feature_flag.rollout_pct%TYPE;
    v_env     feature_flag.env_name%TYPE;
  BEGIN
    v_env := COALESCE(p_env, current_env);

    SELECT enabled, rollout_pct
      INTO v_enabled, v_pct
      FROM feature_flag
     WHERE flag_key = p_flag_key
       AND env_name = v_env
       AND (expires_at IS NULL OR expires_at > SYSTIMESTAMP);

    IF v_enabled = 'N' THEN
      RETURN FALSE;
    END IF;

    IF v_pct >= 100 THEN
      RETURN TRUE;
    END IF;

    IF p_subject IS NULL THEN
      RETURN FALSE;
    END IF;

    RETURN bucket(p_flag_key || ':' || p_subject) < v_pct;
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      RETURN FALSE;
  END;
END;
/

Feature Flagは「存在しない場合はOFF」を基本にすると安全です。逆に、障害時の停止スイッチなどは、設定が読めない場合のデフォルトを慎重に決める必要があります。

業務処理から使う例

業務処理側では、設定値やフラグの取得方法を意識しないようにします。次の例では、外部通知のON/OFFとリトライ回数を設定テーブルから取得しています。

use-config-in-procedure.sql
CREATE OR REPLACE PROCEDURE send_order_notice(
  p_order_id NUMBER,
  p_user_id  VARCHAR2
) AS
  v_retry_count NUMBER;
BEGIN
  IF NOT feature_flag_pkg.is_enabled('ORDER_NOTICE', p_user_id) THEN
    RETURN;
  END IF;

  v_retry_count := app_config_pkg.get_number(
    p_key     => 'order_notice.retry_count',
    p_default => 3
  );

  DBMS_APPLICATION_INFO.SET_ACTION('send_order_notice');

  -- 実際の送信処理
  -- retry_count を使って外部連携する
END;
/

運用時の追跡性を上げるなら、フラグ判定や重要設定の値をログに残すのも有効です。処理名や相関IDを残す設計は DBMS_APPLICATION_INFOを使ったインストゥルメンテーション設計 と組み合わせると扱いやすくなります。

変更監査ログを必ず残す

設定値やFeature Flagは、デプロイなしで本番挙動を変えられる強い仕組みです。そのため、誰が、いつ、何を、どの値からどの値へ変えたのかを必ず残します。更新用のプロシージャを用意し、アプリや運用者が直接UPDATEしない形にすると統制しやすくなります。

config-audit-ddl.sql
CREATE TABLE app_config_audit (
  audit_id     NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  target_table VARCHAR2(30) NOT NULL,
  target_key   VARCHAR2(100) NOT NULL,
  env_name     VARCHAR2(30) NOT NULL,
  old_value    VARCHAR2(4000),
  new_value    VARCHAR2(4000),
  reason       VARCHAR2(1000),
  changed_by   VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV','SESSION_USER') NOT NULL,
  changed_at   TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
);
feature-flag-update-api.sql
CREATE OR REPLACE PACKAGE feature_flag_admin AS
  PROCEDURE set_enabled(
    p_flag_key VARCHAR2,
    p_env      VARCHAR2,
    p_enabled  CHAR,
    p_reason   VARCHAR2
  );
END;
/

CREATE OR REPLACE PACKAGE BODY feature_flag_admin AS
  PROCEDURE set_enabled(
    p_flag_key VARCHAR2,
    p_env      VARCHAR2,
    p_enabled  CHAR,
    p_reason   VARCHAR2
  ) IS
    v_old feature_flag.enabled%TYPE;
  BEGIN
    IF p_enabled NOT IN ('Y', 'N') THEN
      RAISE_APPLICATION_ERROR(-20021, 'p_enabled must be Y or N');
    END IF;

    SELECT enabled
      INTO v_old
      FROM feature_flag
     WHERE flag_key = p_flag_key
       AND env_name = p_env
     FOR UPDATE;

    UPDATE feature_flag
       SET enabled = p_enabled,
           updated_by = SYS_CONTEXT('USERENV','SESSION_USER'),
           updated_at = SYSTIMESTAMP
     WHERE flag_key = p_flag_key
       AND env_name = p_env;

    INSERT INTO app_config_audit(
      target_table, target_key, env_name, old_value, new_value, reason
    ) VALUES (
      'FEATURE_FLAG', p_flag_key, p_env, v_old, p_enabled, p_reason
    );
  EXCEPTION
    WHEN NO_DATA_FOUND THEN
      RAISE_APPLICATION_ERROR(-20020, 'feature flag not found: ' || p_flag_key);
  END;
END;
/

監査ログは自治トランザクションにしたくなる場面もありますが、設定変更本体だけ失敗して監査だけ残ると混乱します。原則は同一トランザクションで扱い、どうしても独立記録したい場合だけ AUTONOMOUS_TRANSACTIONの注意点 を踏まえて設計します。

キャッシュする場合の注意

設定値を毎回SELECTすると、呼び出し頻度が高い処理ではコストになります。ただし、安易にパッケージ変数や RESULT_CACHE に載せると、変更が即時反映されない問題が出ます。即時切替が必要なフラグと、再起動や明示リロードでよい設定を分けて考えます。

即時反映したい毎回DB参照、または短いTTLを持つキャッシュにします。障害停止スイッチはこちらです。
頻繁に参照するが変更は少ないパッケージ変数に読み込み、管理APIでリロードできるようにします。
セッション単位で固定したいログイン時や処理開始時に読み込み、処理中は同じ値を使います。
完全に固定でよい設定テーブルではなく定数パッケージやDDL管理の方が適しています。
config-cache-refresh.sql
CREATE OR REPLACE PACKAGE app_config_cache AS
  PROCEDURE reload;
  FUNCTION get_cached(p_key VARCHAR2) RETURN VARCHAR2;
END;
/

CREATE OR REPLACE PACKAGE BODY app_config_cache AS
  TYPE t_map IS TABLE OF VARCHAR2(4000) INDEX BY VARCHAR2(100);
  g_values t_map;

  PROCEDURE reload IS
  BEGIN
    g_values.DELETE;

    FOR r IN (
      SELECT config_key, value_text
        FROM app_config
       WHERE env_name = SYS_CONTEXT('APP_CTX','ENV_NAME')
         AND enabled = 'Y'
         AND effective_from <= SYSTIMESTAMP
         AND (effective_to IS NULL OR effective_to > SYSTIMESTAMP)
    ) LOOP
      g_values(r.config_key) := r.value_text;
    END LOOP;
  END;

  FUNCTION get_cached(p_key VARCHAR2) RETURN VARCHAR2 IS
  BEGIN
    RETURN g_values(p_key);
  END;
END;
/

パッケージ変数キャッシュはセッションごとに別です。接続プールを使っている場合、どのセッションがいつリロードされたか分かりにくいため、即時停止フラグには向きません。

環境差分の持ち方

環境差分は、env_name で分けるのが基本です。ただし、環境名をアプリコードへ直接書くと移行に弱くなるため、アプリケーションコンテキストや接続時初期化で環境名をセットします。

app-context-env.sql
CREATE CONTEXT app_ctx USING app_ctx_admin;

CREATE OR REPLACE PACKAGE app_ctx_admin AS
  PROCEDURE set_env(p_env_name VARCHAR2);
END;
/

CREATE OR REPLACE PACKAGE BODY app_ctx_admin AS
  PROCEDURE set_env(p_env_name VARCHAR2) IS
  BEGIN
    DBMS_SESSION.SET_CONTEXT('APP_CTX', 'ENV_NAME', p_env_name);
  END;
END;
/

BEGIN
  app_ctx_admin.set_env('PROD');
END;
/

テスト時は同じ仕組みで TESTCI をセットすれば、コードを変えずに設定だけ差し替えられます。CI/CDでのリリース手順と合わせる場合は PL/SQLコードデプロイの自動化とEdition管理 とつなげると整理しやすいです。

権限設計と秘密情報の扱い

設定値テーブルは便利ですが、誰でも更新できると本番事故の入口になります。参照権限と更新権限を分け、更新は管理パッケージ経由にします。また、DBパスワードや外部APIキーなどの秘密情報は、一般設定テーブルに平文で置かない方が安全です。

  • 業務処理スキーマにはSELECTまたは実行パッケージ権限だけを付与する
  • 設定更新は管理ロールと管理パッケージに限定する
  • 秘密情報はOracle Wallet、外部Secret Manager、暗号化テーブルなどへ分離する
  • Feature Flag変更には理由、承認者、期限を必ず残す
  • 本番の即時切替は監査ログと運用手順をセットにする

権限不足やロール経由権限の落とし穴は ORA-01031の原因と対処 で詳しく扱っています。

運用ルールを決めておく

Feature Flagは便利ですが、放置されるとシステムの分岐が増え続けます。作成時点で期限と撤去条件を決め、リリース完了後に不要なフラグを削除する運用を入れます。

作成時目的、対象環境、デフォルト値、期限、戻し方を記録します。
変更時理由、変更者、変更前後の値、承認者を監査ログへ残します。
障害時停止フラグは即時反映できる設計にし、キャッシュ対象から外します。
リリース後恒久化するならコードへ取り込み、不要なフラグと分岐を削除します。

まとめ

PL/SQLの設定値管理とFeature Flagは、デプロイなしで本番挙動を調整できる強力な仕組みです。設定値テーブル、型安全な取得パッケージ、Feature Flag判定、監査ログ、権限設計をセットで作ることで、ハードコードを減らしつつ運用変更に強い構成にできます。

一方で、設定化は万能ではありません。即時反映が必要な値、キャッシュしてよい値、秘密情報、恒久的な仕様値を分け、Feature Flagには期限と撤去条件を持たせることが重要です。便利さと統制の両方を設計に含めておくと、リリース後の事故を減らしながら柔軟な運用ができます。