NGW開発BLOG
← 記事一覧へ
·#002

定数をコードでなくデータベースに置く団体差を吸収する仕組み

団体ごとに変わる設定値を定数としてDBに持ち、起動時にdefineする設計。その利点と、型・存在をコード側で守る契約について。

grep しても見つからない define

NGWの保守を引き継いだエンジニアが、最初に戸惑うことがある。コードのあちこちで _SOME_FLAG_ のような定数が参照されているのに、その定数を define している箇所をリポジトリ全体で grep しても、どこにもヒットしないのだ。

種を明かせば単純である。この定数は、コードのどこにも書かれていない。値の出所はデータベースの設定テーブルであり、アプリケーションの起動時(接続確立時)に、そのテーブルの全行を読み込んで、実行時に一括で define している。つまり値の実体は、ソースではなくデータの側にある。

なぜこんな回りくどいことをするのか。答えは、同じひとつのコードベースを、設定の異なる複数の団体で同時に運用したいからだ。

変わるものはデータに追い出す

自治体向けのシステムでは、団体ごとに「振る舞い」が微妙に違う。1日の標準勤務時間、ある機能を使うか使わないか、何らかの閾値。こうした値をコードに直書きすると、団体が増えるたびにコードが枝分かれし、やがて分岐の森で身動きが取れなくなる。

そこで、変わる可能性のある値を設定テーブルの1行として持たせる。起動時の初期化は、概念的にはこれだけだ。

// 設定テーブルの全行を読んで定数として展開する
$rows = $pdo->query('SELECT name, value FROM app_settings')->fetchAll();
foreach ($rows as $r) {
    define($r['name'], $r['value']); // 例: define('_WORK_HOURS_', '7.75')
}

この設計の効き目は運用面で大きい。新しい設定を増やしたいとき、やることは「テーブルに行を1つ足す」だけで、コードには一切手を入れない。団体ごとの挙動の切り替えも、その団体のDBの値を変えるだけで済む。再デプロイなしに振る舞いを変えられる ── これは要するに feature flag、設定の外部化の一種である。コードは団体差を知らないまま、すべての団体で同じように動く。

データ化した値の型と存在は、コードが守る

美しい仕組みには対価がある。この方式には、意識しておくべきトレードオフが3つある。

  1. 追跡性が落ちる。値がソースに現れないので、「この定数はどこで何のために使うのか」は、定義側ではなく参照側を辿って理解するしかない。grep で定義に飛べない不便は、設計の裏返しとして受け入れることになる。
  2. 型の問題。DB由来の値は文字列として define されることが多い。すると数値のつもりの定数が実は '7.75' という文字列で、厳格比較(===)や型に敏感な分岐で足をすくわれる。近年の言語バージョンほど型に厳しくなっているので、ここは意図的に型を寄せておくべきだ。
  3. 存在の問題。必要な行がDBに登録されていなければ、その定数は未定義のまま参照され、エラーになる。データ側の不備が、そのままコードの実行時エラーに化ける。

だから参照側に、存在チェックと既定値のフォールバックという「契約」を用意しておく。

function cfg(string $name, $default) {
    return defined($name) ? constant($name) : $default;
}

$hours = (float) cfg('_WORK_HOURS_', 8.0); // 未登録でも落ちない・型も寄せる

たった数行だが、これがあるとないとで堅牢性がまるで違う。定数がなくても既定値で動き、文字列を数値に寄せることで比較の事故も防げる。

0 と 1 にも、名前を与える

この「定数化」の発想は、団体差のような可変値だけに向けられているわけではない。もっと素朴な値 ── 01 にも及んでいる。このシステムでは、0_NO_1_YES_ として定数化している(これらも先の設定テーブル由来の定数の仲間だ)。

一見どうでもいい工夫に見えるかもしれない。だが、これでロジックの読みやすさが格段に変わる。

// 生の数値: この 1 は何を意味する?
if ($privacyFlag === 1) { ... }

// 名前を与える: 意図がそのまま読める
if ($privacyFlag === _YES_) { ... }

01 は、プログラムの中で最も基本的で、最も多用される値だ。だがそれゆえに厄介でもある。素の 01 がそこに書かれていても、「この 0 は何を表す 0 なのか」を読み手が頭の中で補わなければ、ロジックの意味は取れない。フラグが立っていないのか、件数がゼロなのか、既定値なのか ── 同じ 0 でも文脈で意味はまるで違う。

_NO_ / _YES_ と名付けておけば、その一語が意図を語る。比較式を目で追った瞬間に「これは"はい"かどうかを見ている」と分かる。コメントを足さずとも、コード自身が意味を持つ。値は同じでも、名前があるかないかで、読み解く速さがまるで違うのだ。

AIとの協働作業での学び

変わるものはコードから追い出して、データにする。団体差のように「同じ振る舞いの、値だけが違う」ものは、分岐ではなく設定として外に出すのが素直だ。再デプロイせずに挙動を変えられる自由は、運用の現場で確かな価値になる。

ただし、データに出した瞬間、その値の型と存在の保証はデータ側から失われる。だからこそ、参照側のコードで「なければ既定値・使う前に型を寄せる」という契約を先に決めておく。外部化の自由と引き換えに手放した安全を、コードの側で拾い直す。この一手間を惜しまないことが、設定をデータに委ねる設計を成立させる条件である。