DBの値をフロントエンドやバックエンドで定数として扱う際のリスクと改善策

概要

今まで、DBの値をFrontendで定数として持って使うケースや、テーブル更新のコストが高いと判断してBackendで値を保持・運用するケースをたまに見てきました。
もちろん軽量なプロジェクトやプロトタイプの製作では有効な場合もありますが、保守性や変更容易性を損なってしまうこともあるので、それについて記載します。

DBの値を定数で持つ例

例えばユーザーが配送の梱包サイズをセレクトボックスなどで選択するUIがあったとします。
これを定数で運用するとどうなるか見ていきます。

フロントエンドの例

// 梱包サイズの定数(実際は定数ファイルなどで定義されていることが多いと思います)
const PACKAGE_SIZES = [
  { id: 1, name: "小" },
  { id: 2, name: "中" },
  { id: 3, name: "大" }
];

// セレクトボックスでの表示
const PackageSizeSelect = () => (
  <select>
    {PACKAGE_SIZES.map(size => (
      <option key={size.id} value={size.id}>{size.name}</option>
    ))}
  </select>
);

このプログラムの何が問題かというと、梱包サイズが増えた場合にフロントエンドの改修が必ず必要になります。
さらに、DBでidやnameが更新された時、合わせてフロントエンドも変更しないと追従できません。
もしかしたら将来、FEで定数として持っていることを知らないエンジニアが、FEを合わせて更新することを忘れてしまう可能性もあり得ます。

もちろん、追加や更新がなく、FEで持っても問題ないケースも存在すると思います。

バックエンドの例

次にバックエンドで定数でもってしまう例を考えてみます。
バックエンドでこうした実装になってしまう例としては、例えば梱包サイズにtypeを表示する要件が追加されたが、運用されているテーブルのカラム追加はコストが高いので、コードで補完しよう、というようなときだと思います(自分の経験上)。

// 梱包サイズとそのタイプをPHPで定義
$packageSizes = $stmt->fetchAll(PDO::FETCH_ASSOC);

// 各梱包サイズにタイプを動的に追加
foreach ($packageSizes as $key => $size) {
    switch ($size['id']) {
        case 1:
            $packageSizes[$key]['type'] = '軽量';
            break;
        case 2:
            $packageSizes[$key]['type'] = '標準';
            break;
        case 3:
            $packageSizes[$key]['type'] = '重量';
            break;
    }
}

// 梱包サイズとタイプをクライアントに送るAPIエンドポイントの例
$app->get('/package-sizes', function ($request, $response) use ($packageSizes) {
    return $response->withJson($packageSizes);
});

この場合、typeをテーブルの情報として持っておらず、PHPで持っているので、テーブルからは読み取れず、実装を見ないとなぜこう表示されているのか分かりません。
またtypeの追加や更新があった場合に、カラムを追加していればSQLの追加や更新だけで対応できますが、この例だと毎回PHPの修正とデプロイが必要になります。

FE、BEの改修がいらなくなるパターンを考える

ではどうすれば良いかというと、BEはテーブルの値を返すだけ、FEはBEから返す値を使うだけ、という構成にすれば良いと思います。

フロントエンド

  const [sizes, setSizes] = useState<PackageSize[]>([]);

  useEffect(() => {
    // APIから梱包サイズのデータを取得する想定
    fetch('/api/package-sizes')
      .then(response => response.json())
      .then(data => setSizes(data))
      .catch(error => console.error('Error fetching package sizes:', error));
  }, []);

  return (
    <ul>
      {sizes.map(size => (
        <li key={size.id}>
          ID: {size.id}, 名前: {size.name}, タイプ: {size.type}
        </li>
      ))}
    </ul>
  );

実装は簡略化して書いてます。
カラムが追加されて、それを新しく表示する場合は対応が必要ですが、梱包のidやname, typeが追加、更新、削除されてもFEの改修は必要がなくなりました。

サーバーサイド

$packageSizes = $stmt->fetchAll(PDO::FETCH_ASSOC);

$app->get('/package-sizes', function ($request, $response) use ($packageSizes) {
    return $response->withJson($packageSizes);
});

バックエンドも梱包のidやname, typeが追加、更新、削除されても改修は基本必要なくなりました。
もちろんこの実装は簡略化しており、更新内容によってはValueObjectの更新だったり、カラム追加などではDTOの更新などをする必要はあると思います。

総合的には毎回FEやBE対応をする必要がなくなって、SQL対応のみで良くなるケースが増え、保守性や変更容易性を損なわない仕組みになったと思います。