ずっと気になってたDrizzle ORMに入門してみたときの備忘録(*´ω`*)
合わせて、Tursoも一緒に使ってみる
version
drizzle-orm
: v0.38.2drizzle-kit
: v0.30.1@libsql/client
: v0.14.0nuxt
: v3.14.1592
とりあえずお試し
「Get Started」を見ながら、setupと簡単な読み書きをしてみる
Get Started with Drizzle and Turso | Drizzle ORM - SQLite
インストール
$ pnpm add drizzle-orm @libsql/client $ pnpm add -D drizzle-kit
- drizzle-orm ... テーブル定義やクエリなどのORM部分
- drizzle-kit ... マイグレーションなどのツールキット
DB接続情報の設定
アプリが使う接続情報を設定する
# .env NUXT_DATABASE_URL= NUXT_DATABASE_AUTH_TOKEN=
nuxt.config.ts
も設定
// nuxt.config.ts export default defineNuxtConfig({ // ...略 runtimeConfig: { database: { url: process.env.NUXT_DATABASE_URL || "", authToken: process.env.NUXT_DATABASE_AUTH_TOKEN || "", } } });
テーブルを用意する
SQLiteの場合は、こんな感じで作れるっぽい
// server/database/schema.ts import { int, sqliteTable, text } from "drizzle-orm/sqlite-core"; /// ユーザテーブル export const usersTable = sqliteTable("users_table", { id: text().primaryKey(), name: text().notNull(), email: text().notNull().unique(), });
Drizzleの設定ファイルを用意する
マイグレーションツールのdrizzle-kit用の設定も用意
// drizzle.config.ts import 'dotenv/config'; import { defineConfig } from 'drizzle-kit'; export default defineConfig({ // Drizzleが生成するファイルの出力先 out: './drizzle', // テーブルなどのschemaファイルの配置場所 schema: './server/database/schema.ts', // DBの種類 dialect: 'turso', // DBの接続情報 dbCredentials: { url: process.env.NUXT_DATABASE_URL || "", authToken: process.env.NUXT_DATABASE_AUTH_TOKEN, }, });
DBをデプロイする
もろもろの設定ができたら、shemaをpushしてみる
scripts
を設定しておくと便利
"scripts": { "db:push": "drizzle-kit push" },
あとは実行すればOK
$ pnpm db:push
まずは読み書き
とりあえず、ユーザの作成とEmailでの検索はこんな感じ
接続情報はuseRuntimeConfig()
から取得
// server/repository/usersRepository.ts import { eq } from "drizzle-orm"; import { User, usersTable } from "../database/schema"; import { drizzle } from "drizzle-orm/libsql"; // usersTableの型定義 export type User = typeof usersTable.$inferInsert; // drizzleクライアントを作成 export function createDrizzleClient() { const config = useRuntimeConfig().database; return drizzle({ connection: { url: config.url, authToken: config.authToken, } }); } // ユーザの作成 interface CreateUserParams { uid: string, name: string, email: string; }; export async function createUser({ uid, email, name }: CreateUserParams): Promise<void> { const db = createDrizzleClient(); const user: User = { id: uid, email, name }; // insert await db.insert(usersTable).values(user); } // Emailでユーザの検索 export async function findUserByEmail(email: string): Promise<User | null> { const db = createDrizzleClient(); // select const res = await db.select().from(usersTable).where(eq(usersTable.email, email)); return res.length > 0 ? res[0] : null; }
SELECTの便利機能
Drizzle Queriesという、select時に便利な機能もあるっぽい
drizzleクライアント作成時にschemaを設定すると、
db.query.usersTableが生えてきて、SELECTが楽にできる
// server/repository/usersRepository.ts import { eq } from "drizzle-orm"; import { User, usersTable } from "../database/schema"; import { drizzle } from "drizzle-orm/libsql"; // usersTableの型定義 export type User = typeof usersTable.$inferInsert; // drizzleクライアントを作成 export function createDrizzleClient() { const config = useRuntimeConfig().database; return drizzle({ connection: { url: config.url, authToken: config.authToken, }, // schemaを設定すると schema: { users: usersTable }, }); } // Emailでユーザの検索 export async function findUserByEmail(email: string): Promise<User | null> { const db = createDrizzleClient(); // db.query.usersTableが生えてくる const res = await db.query.users.findFirst({ where: eq(usersTable.email, email) }); return res ?? null; }
update/delete
ちなみに、更新や削除はこんな感じ
interface UpdateUserParams { name: string, email: string; }; export async function updateUser(uid: string, params: UpdateUserParams): Promise<void> { const db = createDrizzleClient(); await db.update(usersTable).set(params).where(eq(usersTable.id, uid)); } export async function deleteUser(uid: string): Promise<void> { const db = createDrizzleClient(); await db.delete(usersTable).where(eq(usersTable.id, uid)); }
基礎知識を知る
「Get Started」でなんとなく理解したので、
構成要素とかもう少し理解を深めてく
左のメニューにある「Fundamentals」あたりを見ていく
テーブル定義/Schemaまわり
schemaはフォルダでもOK
import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: 'postgresql', // 'mysql' | 'sqlite' | 'turso' // ファイルの場合 schema: './src/db/schema.ts', // フォルダの場合 schema: './src/db/schema', })
詳しくは、drizzle.config.tsのマニュアルあたりに
対応しているDBの機能
- Table/View/Enum etc
- PostgreSQL onlyでSchemas / Sequencesも
詳しくは、メニュー左の「MANAGE SCHEMA」あたりに
テーブル定義
ざっくり書くとこんな感じらしい
<DBのカラム名>
は任意で、省略すると<tsのカラム名>
と同じになる
import { sqliteTable, integer } from "drizzle-orm/sqlite-core" export const <tsのテーブル名> = sqliteTable('<DBのテーブル名>', { <tsのカラム名>: <カラムの型>("<DBのカラム名>") });
TypeScriptだとキャメルケースが一般的だけど、
DBではスネークケースが一般的なので、こんな感じにできるよう
import { sqliteTable, integer } from "drizzle-orm/sqlite-core" export const users = pgTable('users', { firstName: text('first_name') })
自動で変換してくれる機能もあるので、そっちを使ってもOK
import { sqliteTable, integer } from "drizzle-orm/sqlite-core" export const users = pgTable('users', { firstName: text() }) // casingで自動変換 const db = drizzle({ connection: { /* 略 */ }, casing: 'snake_case' });
共通的なカラム
created_at
など共通的なカラムも再利用できるよう
// 共通のカラム const timestamps = { updated_at: timestamp(), created_at: timestamp().defaultNow().notNull(), deleted_at: timestamp(), } // 個別のカラム export const users = pgTable('users', { id: integer(), ...timestamps }) export const posts = pgTable('posts', { id: integer(), ...timestamps })
制約(constraints)
import { sql } from "drizzle-orm"; import { integer, sqliteTable, /* 略 */ } from "drizzle-orm/sqlite-core"; const table = sqliteTable('table', { // default int1: integer('int1').default(42), int2: integer('int2').default(sql`(abs(42))`), // not null numInt: integer('numInt').notNull(), // primary id: integer("id").primaryKey(), }); // unique const table = sqliteTable('table', { // unique id: int('id').unique('custom_name'), // インデックス名指定 id2: int('id2').unique(), // 省略すると`<table名>_<column名>_unique`になる }, (table) => ({ // 複合キー unq: unique('custom_name').on(t.id, t.name) unq2: unique().on(t.id, t.name), })); // primary key const table = sqliteTable('table', { // primary id: integer("id").primaryKey(), authorId: integer("author_id"), bookId: integer("book_id"), }, (table) => ({ // 複合キー pk: primaryKey({ columns: [table.bookId, table.authorId] }), })); // foreign key export const user = sqliteTable("user", { id: integer("id").primaryKey({ autoIncrement: true }), }); export const book = sqliteTable("book", { // foreign key authorId: integer("author_id").references(() => user.id) });
indexes
export const user = sqliteTable("user", { id: integer("id").primaryKey({ autoIncrement: true }), name: text("name"), email: text("email"), }, (table) => { return { nameIdx: index("name_idx").on(table.name), emailIdx: uniqueIndex("email_idx").on(table.email), }; });
view
export const user = sqliteTable("user", { id: integer().primaryKey({ autoIncrement: true }), role: text().$type<"admin" | "customer">(), }); export const customersView = sqliteView("customers_view").as((qb) => { return qb.select({ id: user.id, name: user.name, email: user.email, }) .from(user) .where(eq(user.role, "customer")); );
クエリ周り
SQLっぽい書き方と、SQLっぽくない書き方(Query)のなど、いろいろ書き方があるっぽい
どちらもで書いても同じクエリが発行され、Drizzle側でいい感じにしてくれるらしい
// SQLっぽい書き方 await db.select().from(posts) .leftJoin(comments, eq(posts.id, comments.post_id)) .where(eq(posts.id, 10)) // SQLっぽくない書き方(Query) const result = await db.query.users.findMany({ with: { posts: true }, });
細かい書き方は左のメニューの「ACCESSS YOUR DATA」にある
集合操作(UNIONなど)
UNION/INTERSECT/EXCEPTなどは、このあたり
トランザクション
トランザクションはこのあたり
await db.transaction(async (tx) => { const [account] = await tx.select({ balance: accounts.balance }).from(accounts).where(eq(users.name, 'Dan')); if (account.balance < 100) { // This throws an exception that rollbacks the transaction. tx.rollback() } await tx.update(accounts).set({ balance: sql`${accounts.balance} - 100.00` }).where(eq(users.name, 'Dan')); await tx.update(accounts).set({ balance: sql`${accounts.balance} + 100.00` }).where(eq(users.name, 'Andrew')); });
DBのマイグレーション
マイグレーションはこのあたり
データベースを真とする(Database first)か、
ソースコードを真とする(Codebase first)か、
の2つの考え方があり、6つのアプローチがある
関連コマンド
drizzle-kit migrate ... マイグレーションの実行 drizzle-kit generate ... マイグレーションファイルの生成 drizzle-kit push ... データベースへのpush drizzle-kit pull ... データベースからのpull
各コマンドの詳細は、左メニューの「MIGRATIONS」にある
6つのアプローチ
1. データベースを他のツールや自身のSQLで管理している場合
ツールなどで実行後、最新のDBからdrizzle-kit pull
して、
DBからtsコードを生成(Database first)
2. コードで管理したいが、マイグレーションファイルは使いたくない場合
tsコードを変更後、drizzle-kit push
して、DBに反映(Codbase first)
3. コマンドでマイグレーションファイルで反映したい場合
tsコード変更後、drizzle-kit generate
でsqlファイルを生成し、
drizzle-kit migrate
で、DBに反映(Codebase first)
4. コード実行時にマイグレーションファイルで反映したい場合
tsコード変更後、drizzle-kit generate
でsqlファイルを生成し、
tsコード上でawait migrate(db);
を実行し、DBに反映(Codebase first)
5. 外部ツールや自分でマイグレーションファイルを反映したい場合
tsコード変更後、drizzle-kit generate
でsqlファイルを生成し、
手動または別のツールで、sqlファイルを実行し、DBに反映(Codebase first)
6. Atlas経由でマイグレーションファイルを反映したい場合
tsコード変更後、drizzle-kit export
でsqlを生成(コンソールに出力)し、
Atlas経由でsqlを実行し、DBに反映(Codebase first)
カスタムマイグレーション
Drizzle Kitの仕組みには乗せたい(drizzle-kit migrate
で反映したい)が、
SQLファイルは自分で書きたい場合、空のSQLファイルを作成することもできるらしい
$ drizzle-kit generate --custom --name=seed-users # => ./drizzle/0001_seed-users.sqlを生成
その他もろもろ
リードレプリカ
Read Replicaにも対応。selectはレプリカ、書き込みはprimaryを自動で選択してくれるっぽい
ロギング
ログ出力とかもあり、カスタマイズもできる
// ロギングの有効化 const db = drizzle({ logger: true }); // custome writer class MyLogWriter implements LogWriter { write(message: string) { // Write to file, stdout, etc. } } const logger = new DefaultLogger({ writer: new MyLogWriter() }); const db = drizzle({ logger }); // custome logger class MyLogger implements Logger { logQuery(query: string, params: unknown[]): void { console.log({ query, params }); } } const db = drizzle({ logger: new MyLogger() });
valibot/zodなど用のプラグイン
いくつかプラグインが用意されていて、
テーブル定義からvalibotのshemaを生成してくれるっぽい
import { pgTable, text, integer } from 'drizzle-orm/pg-core'; import { createInsertSchema } from 'drizzle-valibot'; import { parse } from 'valibot'; // table const users = pgTable('users', { id: integer().generatedAlwaysAsIdentity().primaryKey(), name: text().notNull(), age: integer().notNull() }); // valibot schema const userInsertSchema = createInsertSchema(users); // parse error const user = { name: 'John' }; const parsed: { name: string, age: number } = parse(userInsertSchema, user); // Error: `age` is not defined // parse success const user = { name: 'Jane', age: 30 }; const parsed: { name: string, age: number } = parse(userInsertSchema, user); // Will parse successfully // execute query await db.insert(users).values(parsed);
生SQLの発行
sql
オペレータでいろいろできるっぽい
デフォルト値の生成関数
デフォルト値の値は、関数で設定できたりするらしい
なので、UUID/UILD/CUID/shortidなどなど、いろいろできそう
import { text, sqliteTable } from "drizzle-orm/sqlite-core"; import { createId } from '@paralleldrive/cuid2'; const table = sqliteTable('table', { id: text().$defaultFn(() => createId()), });
onDeleteやonUpdate時の動作
外部キー制約の部分で、指定できるっぽい
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core'; export const users = pgTable('users', { id: serial('id').primaryKey(), }); export const posts = pgTable('posts', { id: serial('id').primaryKey(), author: integer('author') // ここで指定 .references(() => users.id, {onDelete: 'cascade'}).notNull(), });
以上!! なんとなくわかった気がする...(*´ω`*)