PrismaとMySQLのタイムゾーン不整合問題とその対応

はじめに

本記事では、PrismaとMySQLを組み合わせた際に発生しやすいタイムゾーン(特にJST/UTC)不整合の問題と、その対応策について解説します。

背景・前提

多くの日本の開発現場では、MySQLサーバーのタイムゾーン設定がJST(日本標準時)になっていることが一般的です。一方、Prisma(Node.js ORM)はDateTime型の値を「タイムゾーン情報なしのISO8601文字列」として扱い、DBサーバ側のタイムゾーン設定や値のタイムゾーンを自動で解釈・変換しません

特に、以下のような前提や制約がある場合に時刻の不整合が発生しやすくなります:

  • DBがクラウドのマネージドサービスで、DBサーバのタイムゾーン設定をJSTに変更できない、またはUTC固定運用が求められる場合
  • Prismaやアプリケーション側はUTCで時刻を扱うが、SQLを直接発行する運用や、DBのデフォルト値・トリガー等でJST時刻が生成される場合
  • チームやプロジェクトでSQLを直打ちする文化があり、ORMを介さずにデータを操作するケースが混在する場合

このような状況下で、アプリケーション・ORM・DB間で時刻のズレや不整合が発生しやすくなります。

実際に、時刻の生成や保存方法が複数存在することで、アプリケーション・ORM・DB間で時刻のズレや不整合が発生しやすくなります。主なパターンは次の3つです。

DB時刻生成・保存方法の主なパターン

1. DB自動生成

  • createdAtupdatedAtは、Prisma schemaの@default(now())によりUTCで自動生成される。
1
2
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @map("updated_at")

2. アプリ生成(new Date())

  • アプリケーション側でnew Date()(UTC)などを使って時刻を生成し、DBに保存するケース。
1
2
3
4
5
6
7
const now = new Date();
await prisma.someModel.create({
	data: {
		// ...他のフィールド
		deletedAt: now,
	}
});

3. DB/SQL直書き

  • SQLを直接発行し、DB側のON UPDATEDEFAULT CURRENT_TIMESTAMPなどの仕組みで、JST時刻が自動生成されるケース。

主な対応策

どの経路(アプリ・ORM・DB)から時刻が生成・保存されても、最終的にJSTで一貫した時刻管理ができるように設計・運用します。

DB自動生成の見直し

Prisma schemaの@default値にdbgenerated("CURRENT_TIMESTAMP(3)")を採用し、JSTで時刻生成を行うように変更。

1
2
createdAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP(3)")) @map("created_at")
updatedAt DateTime @default(dbgenerated("CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)")) @map("updated_at")

補足:

  • dbgenerated("NOW(3)")も同様の動作となりますが、標準性や移植性の観点からはCURRENT_TIMESTAMPの利用を推奨します。
  • なお、PrismaのMySQL対応には既知のバグがあり、DATETIME(3)型のカラムに@default dbgenerated("CURRENT_TIMESTAMP(3)")を設定すると、スキーマ変更がなくてもprisma migrate dev実行時に同内容のmigrationファイルが繰り返し生成される場合があります。 この場合、migrationファイルの内容を確認し、実質的な差分がない場合は手動で削除するなどの運用をする必要があります。

アプリ生成の補正

Prisma Clientの拡張($extends)で、書き込み時にUTC→JST、取得時にJST→UTCの補正を自動化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const setOffsetTime = (object: any, offsetTime: number) => {
	if (object === null || typeof object !== 'object') return;
	for (const key of Object.keys(object)) {
		const value = object[key];
		if (value instanceof Date) {
			object[key] = new Date(value.getTime() + offsetTime);
		} else if (value !== null && typeof value === 'object') {
			setOffsetTime(value, offsetTime);
		}
	}
};

return new PrismaClient().$extends({
	query: {
		$allModels: {
			async $allOperations({args, query}) {
				const offsetTime = 9 * 60 * 60 * 1000; // JST補正
				setOffsetTime(args, offsetTime);
				const result = await query(args);
				setOffsetTime(result, -offsetTime);
				return result;
			},
		},
	},
});

DB/SQL直書き

DBやSQLで直接時刻を生成・更新する場合も、MySQLサーバー自体のタイムゾーン設定がJSTで統一されていれば、DEFAULT CURRENT_TIMESTAMPON UPDATE CURRENT_TIMESTAMPによる自動生成時刻もJSTとなります。 そのため、アプリやORM経由でJSTに統一した時刻と、DB/SQL直書きで生成される時刻にズレが生じず、一貫性が保たれるため問題ありません。


結果・まとめ

これらの対応により、DB・アプリ・Prisma間の時刻不整合が解消されます。