TypeScript(NestJS)で新規プロダクトのバックエンドを開発する中で、既存のMySQLデータベースにORM(Object Relational Mapper)を導入しました。
最初はPrismaを採用していたのですが、既存DBの日付カラム(0日付)を扱えないという問題にぶつかり、最終的にDrizzleへ切り替えました。
この記事では、その経緯と背景を整理します。
ORMを採用した理由
新規プロダクトはTypeScript(NestJS)で構築しており、DBとアプリケーション間のデータを型安全に扱いたいというのが大きな目的でした。
PrismaのようなORMは、DBスキーマをもとにTypeScriptの型を自動生成してくれます。
たとえば、MySQLのDATETIME
カラムを持つテーブルをselect
すると、アプリケーション側でも自動的にDate
型として扱えるようになります。
手動で型変換を行う必要がないため、開発効率や保守性が格段に上がります。
自前で型変換するのは大変なので採用しました。
問題:PrismaではDatetime型の0日付を扱えない
ここで問題が発生しました。
Prismaでは、MySQLの0000-00-00 00:00:00
という「0日付」をJavaScriptのDate
型に変換できません。
MySQLではDATETIME
やDATE
型のデフォルト値を0000-00-00
に設定できますが、PrismaがこれをパースするときにP2020エラーが発生します。
Prismaは0000のDateオブジェクトをJSのDateにパースするときに0日付はJSでパースできません。
下記のようなP2020エラーが発生します。
code: 'P2020',
meta: { modelName: 'member',
details: 'The column created contained an invalid datetime value with either day or month set to zero.' },
clientVersion: '6.14.0'
公式でも「0000日付」はサポート外と明記されています。
既存DBでは、「期限なし」「未登録」などを表すために0日付を多用しておりました。
ログイン管理や期限管理などでDATETIME
カラムを扱う箇所が多く、非常に困りました。
試した対策
Prismaのスキーマ定義側で型をString
にしてみたり、dbgeneration
を使って強制的に0日付をデフォルトにしてみたりと検証を行いました。
model member {
mid Int @id @default(autoincrement())
created DateTime @default(dbgenerated("'0000-00-00 00:00:00'")) @db.DateTime(0)
expire_time DateTime @default(dbgenerated("'0000-00-00 00:00:00'")) @db.DateTime(0)
}
あるいは、文字列として定義しても:
model member {
mid Int @id @default(autoincrement())
created String @db.VarChar(25)
expire_time String @db.VarChar(25)
}
結果は同じでした。
Prisma内部のRust製ドライバはMySQLの型情報をもとにDateTimeとして扱うため、型定義を変えても0日付を受け取った時点でパースエラーになります。
Prismaのアーキテクチャを理解する
ここで重要なのは、Prismaの内部構造です。
PrismaはNode.jsで動くように見えて、実際のDBとのやり取りはRustで実装された「Prisma Query Engine」が担っています。
+------------------+ +-------------------+ +----------------+
| Node.js (JS) | <-----> | Prisma Query | <-----> | MySQL DB |
| └─ PrismaClient | | 内蔵ドライバ(Rust)| | (TCP 接続) |
+------------------+ +-------------------+ +----------------+
このエンジンにはMySQL用ドライバが内蔵されており、アプリケーション側からドライバの挙動を変更することはできません。
そのため、0000-00-00
をstring
扱いしたり、NULLに変換するような制御も不可能でした。
つまりいくらスキーマファイルで定義しようが、アプリケーション側から内蔵ドライバの操作を行うことはできません。
最近のバージョンだとより柔軟に対応させるためか、任意でデータベースドライバを組み合わせることができるようです。
ただ、Prismaを使うための制約が多く、検証していませんが、こちらは不採用にしました。
対策:Drizzleへ乗り換え
最終的に、Drizzle ORMへ切り替えました。
DDLを変更して既存DBを更新する選択肢もありましたが、既に稼働中のサービスだったため、影響範囲が大きすぎると判断しました。
もし新規DBを構築できる環境なら、ORMに合わせてDB設計を見直すのが一番早いです。
Drizzleの仕組みと柔軟性
Drizzleは、Prismaと違いRustエンジンを持たず、Node.js内で完結します。
内部では公式のmysql2
ドライバを直接利用しており、アプリケーション側からオプションを制御できます
+-----------------------+ +-----------------------------+
| Node.js (TypeScript) | ----> | MySQL / PostgreSQL / SQLite |
| └─ Drizzle ORM | | (Native DB over TCP) |
| ├─ drizzle-orm | +-----------------------------+
| ├─ mysql2 / pg | (JSドライバを直使用)
+-----------------------+
これにより、日付の扱いを文字列として受け取るように設定可能です。
mode: 'string'
にすることで、ドライバが0000-00-00 00:00:00
を文字列として安全に扱ってくれます。この柔軟性が決定打でした。
export const member = mysqlTable(
'member',
{
memberId: int({ unsigned: true }).autoincrement().notNull(),
created: datetime({ mode: 'string' })
.default('0000-00-00 00:00:00')
.notNull(),
expireTime: datetime('expire_time', { mode: 'string' })
.default('0000-00-00 00:00:00')
.notNull(),
},
まとめ: ドライバを制御できるかどうかが分岐点
今回のポイントです。
ORMがどのドライバを使い、アプリケーションから制御できるか。
ORM | ドライバ | 挙動制御 | 0日付の扱い |
---|---|---|---|
Prisma | 内蔵(Rust) | ❌ 固定 | ❌ サポート外 |
Drizzle | mysql2(JS) | ✅ 自由に設定可能 | ✅ 文字列で扱える |
Prismaは強力で高機能ですが、既存DBを持つ環境では融通が利きにくい。
一方Drizzleは軽量かつ柔軟で、既存スキーマを壊さず導入できるのが大きな利点です。
新しいORMを導入するときは、DBスキーマとの互換性とドライバの制御範囲を確認すること。
既存DBがある場合、柔軟に設定できるDrizzleのほうが軍配が上がりそうです。
私としても勉強になりました!