前回の続き。Drizzle ORMをちょっと触ってみたときに、
いろいろ調べてみたときの備忘録(*´ω`*)
SQLiteにはdate/time型がない
残念なことに、SQLiteにはDate/Time/DateTimeの形がなく、
以下の形式の文字列(text
)か数値(integer
)で保存する
- ISO-8601 ...
text
- Unix timestamp ...
intager
いくつか便利な関数が用意されていて、それを利用する形になる
date()
...YYYY-MM-DD
形式のtext
time()
...HH:MM:SS
やHH:MM:SS.SSS
形式のtext
datetime()
...YYYY-MM-DD HH:MM:SS
などのtext
strftime()
... 指定したフォーマットのtext
unixepoch()
... unix timestampのinteger
Drizzleで用意されている型
Drizzleだとこんな感じで、定義できる
import { sql } from "drizzle-orm"; import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; export const datetimeTestTable = sqliteTable("datetime_test", { // 10桁(秒)のinteger timestamp: integer({ mode: "timestamp" }).notNull().default(sql`(unixepoch())`), // 13桁(ミリ秒)のinteger timestamp_ms: integer({ mode: "timestamp_ms" }).notNull().default(sql`(unixepoch())`), // UTCのISO8601形式の文字列(2025-01-01T00:00:00.000Z) datetime_iso_str: text().notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), });
DatetimeTestTable
の各型はこんな感じ
export type DatetimeTestTable = typeof datetimeTestTable.$inferInsert; // type DatetimeTestTable = { // timestamp?: Date | undefined; // timestamp_ms?: Date | undefined; // datetime_iso_str?: string | undefined; // }
DB上でのinteger
/text
どちらも使えるが、
integer({ mode: "timestamp" })
など、mode
を指定すると、
TypeScript上ではDate
として扱うことができる
integer版の作成日時など
共通的なカラムとして扱う形式だととこんな感じ
import { integer, sqliteTable } from "drizzle-orm/sqlite-core"; // 共通のカラム const timestamps = { // 作成日時 created_at: integer({ mode: "timestamp" }).notNull() .default(sql`(unixepoch())`), // 更新日時 updated_at: integer({ mode: "timestamp" }).notNull() .default(sql`(unixepoch())`) .$onUpdate(() => sql`(unixepoch())`), // 削除日時 deleted_at: integer({ mode: "timestamp" }), }
ISO8601版の作成日時
ISO8601文字列で、DBに保存したい場合はこんな感じ
const iso8601Datetimes = { created_at: text().notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), updated_at: text().notNull() .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`) .$onUpdate(() => sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), deleted_at: text(), };
ISO8601版でDateで受け取りたい
とはいえ、DB上ではtext
でも、
TypeScript上では、Date
で扱いたい。。。
ので、DrizzleのCustom Typesという機能を使って実現してみる
Custom Typesとは
text()
やinteger()
みたいなのを自分で作れる機能。
ドキュメントには、PostgresとMySqlしか書かれていないが、
SQLiteにも用意されているっぽい
Custom Typesを使ってみる
customType
で型を自作してみるとこんな感じ
import { customType } from "drizzle-orm/sqlite-core"; // TypeScriptではDate、DB上ではISOのtextとして扱うCustom Types const isoDateTime = customType<{ data: Date; // TypeScript上の型 driverData: string; // DB上の型 }>({ // DB上の型 dataType: (): string => "text", // TypeScript -> DBの変換 toDriver: (value: Date): string => value.toISOString(), // DB -> TypeScriptの変換 fromDriver: (value: string): Date => new Date(value), });
テーブル定義はこんな感じになる
export const isoDatetimeTestTable = sqliteTable("iso_datetime_test", { // 作ったisoDateTimeを使ってカラムを定義 created_at: isoDateTime().notNull().default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`), }); // 型推論でDate型になる export type IsoDatetimeTest = typeof isoDatetimeTestTable.$inferInsert; // type IsoDatetimeTest = { // created_at?: Date | undefined; // }
他のオプションはこんな感じ
customType<{ data: Date; // TypeScript上の型 driverData: string; // DB上の型 config: Record<string, any>, // 任意のconfig configRequired: false, // configの必須フラグ // カスタムタイプとして、notNullか // trueの場合、常に.notNull()が付与された状態になる(DDLには含まれない) notNull: false, // カスタムタイプとして、デフォルト値があるか // trueの場合、常に.default()系が設定された状態になる(DDLには含まれない) default: false, }>();
日付ライブラリでもOK
なので、「Dateの代わりにDayjsを使う」とかもOK
import { customType } from "drizzle-orm/sqlite-core"; import { Dayjs } from "dayjs"; // Dateの代わりにDayjsを使うver const isoDateTimeDayjs = customType<{ data: Dayjs; // TypeScript上の型 driverData: string; // DB上の型 }>({ // DB上の型 dataType: (): string => "text", // TypeScript -> DBの変換 toDriver: (value: Dayjs): string => value.toISOString(), // DB -> TypeScriptの変換 fromDriver: (value: string): Dayjs => new Dayjs(value), });
以上!! これでだいぶ捗るぞ。。。(*´ω`*)