🐼

Prisma の enum をやめたら、補完と整合性の管理でつらくなった

に公開

はじめに

Prisma の enum をやめたら補完が効かなくなった。その補完を復活させたら、今度は型定義の二重管理に悩まされることに。この記事では、そんな開発中の試行錯誤をまとめています。

PeopleX のエンジニアの桑原(@kyuwabara)です。
好きな MySQL の DDL は ALTER TABLE ... ADD COLUMN ... AFTER ... です。
ここ最近は慣れ親しんだ Go 言語を離れ、TypeScript / Prisma / NestJS を採用したプロジェクトにアサインされてバックエンド開発をしています。Go 言語は長くやっていたけど Node.js は始めたばかり。慣れない開発環境に戸惑うこともありますが、アサイン直後に一番違和感があったのが、 DB のカラムに enum が多用されていることでした。

RDBMS における enum 型は、使いどころをよく考えないと大変つらいことになるのはご存じの通りかと思います。聖書にもそう書かれています。それが息をするように使われていて大変驚きました。どうやら Prisma の流儀らしく[1]、その時はそういうものかと思っていました。

ですが、起こり得る問題は起こるもので、開発が進み、エンジニアが増えるに連れて enum への値の追加頻度が上がり、マイグレーションの順番やリリース手順に気を遣う必要が出てきました。この状況を解消すべく、一部の enum のカラムを varchar にしたところ、それはそれで別の問題を引き起こすことになりました。

本記事は、Prisma enum を使わないという選択をした背景と、そこから始まった試行錯誤についての記録です。同じように困った誰かの参考になると幸いです。

なぜ enum をやめようと思ったのか

確かに実際コードを書いてみると enum はわりと便利でした。Prisma のスキーマに定義すれば値の候補が明示的になり、コード上でもそのまま型として使えます。ですが、DB のカラムが enum 型 になるという点で、あらゆる状況で enum を採用するのは問題があると考えました。
DB のカラムに enum を使う場合、以下のような問題が発生することがあります。

  • 取り得る値を調べるのが難しい
  • 値の追加・変更・削除が難しい

前者は Prisma がコードを生成してくれるのであまり問題にはなりません。
しかし後者は、値を 1 つ追加するだけでもマイグレーションの順序やデプロイ手順に気を遣う必要がありますし、頻繁に要素が追加・更新されるような性質のものに対して適用するのは好ましくありません。
本来ならば要素自体を管理するテーブルに切り出してリレーションを作成するのがいいのですが、既存コードへの改修が大きくなってしまうので、今回は enum 型を varchar 型に変更し、データの整合性はアプリケーション側で専用のユニオン型を持つことで担保するようにしました。

enum をやめたら補完が効かなくなった

要素の追加が多いカラムを、Prisma のスキーマ上では単なる String 型に変更しました。
これによって要素の追加を安心して行えるようになりましたが、今度は Prisma 使用時に型の補完が効かないという問題が起きました。
イメージとしては次のような感じです。

const user = prisma.user.findMany({
  where: {
    status: 'ACTIVE', // <- ここで補完・型チェックが効かない
  }
})

Prisma 的にはただの String なので当たり前っちゃ当たり前ですが、Prisma の使用時に補完をしてくれないのは困ります。テストコード内では直接 Prisma を使ってテストデータを作成することもよくあり、開発の快適性・安全性が損なわれます。かといって補完のために enum を使い続けると今後の開発が持続できない。コード補完と運用の柔軟性、どちらも確保できるやり方を探す必要が出てきました。

補完を効かせたら定義が分散した

困ったのでボスに相談したところ、prisma-json-types-generatorを使えば String の値でも入力内容を制限でき、補完も効くと教えてくれました。
https://github.com/arthurfiorette/prisma-json-types-generator
こんな感じで指定すると、String で enum と同様のことができます。

model User {
  id   String
  name String
  /// !['ACTIVE' | 'INACTIVE']
  status String
}

補完も効くようになって、これで問題解決!と思った瞬間、別の問題にすり替えただけであることに気付いて愕然としました。アプリケーション側に定義したユニオン型と schema に付与したコメント、全く同じ記述を 2 箇所で行っています。どう見てもミスの温床です。これは・・・どうなんだ・・・?

まとめ

結局ボスと相談し、コメントと型定義の二重管理を許容する運用に現在は落ち着きました。
今回は運用の難しさ、型補完の欠落、情報の二重管理という 3 つの中から情報の二重管理を受け入れることとなりましたが、もっといい解決策があるのではないかという気はしています。別テーブルを用意する王道パターンならもう少しきれいにできそうな予感がありますが、考え切れていない落とし穴がありそうです。
こういうときは複数の手札からメリデメを比較してよりよい方法を考えるのが一般的だと思いますが、その手札のパターンが少なくて選択肢に幅が出せないところが、まだまだ開発環境に慣れていないなと感じます。
今回の件で、技術選定や設計に唯一絶対の正解は存在せず、常に状況やフェーズに応じたトレードオフがあると再認識しました。自分のような(その環境の)初心者にとっては何がよい選択なのか判断が難しいことも多く、もっと習熟が必要だなといったところですね。

もしこの記事の内容に対して、もっと冴えたやり方や既に存在する定番のやり方をご存じでしたら、ぜひコメントでお教えいただけると嬉しいです。

脚注
  1. 聞いただけで裏取りはしていません。ちゃんと調べないといけませんね。 ↩︎

PeopleXテックブログ

Discussion