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パッケージ側で型変換する方が、設定の追加に強くなります。
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_varchar2、get_number、get_boolean のように型ごとの関数を公開します。
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は、機能名、対象環境、有効/無効、段階リリース率、説明、期限を持たせます。期限を持たせることで、リリース後に不要になったフラグを放置しにくくなります。
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など、安定したキーからハッシュ値を作り、ロールアウト率以下なら有効にする設計にすると、同じユーザーで結果が揺れません。
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とリトライ回数を設定テーブルから取得しています。
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しない形にすると統制しやすくなります。
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
);
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 に載せると、変更が即時反映されない問題が出ます。即時切替が必要なフラグと、再起動や明示リロードでよい設定を分けて考えます。
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 で分けるのが基本です。ただし、環境名をアプリコードへ直接書くと移行に弱くなるため、アプリケーションコンテキストや接続時初期化で環境名をセットします。
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;
/
テスト時は同じ仕組みで TEST や CI をセットすれば、コードを変えずに設定だけ差し替えられます。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には期限と撤去条件を持たせることが重要です。便利さと統制の両方を設計に含めておくと、リリース後の事故を減らしながら柔軟な運用ができます。
