Drizzleってみる。
フロントエンドのORMといえばprismaがいっぱいヒットするけど、ひねくれもんなのでDrizzle ORMをいじくってみることにする。
決して名前の響きで選んだわけではない。
と言うことで早速、セットアップから。
元はこのあいだ作ったプロジェクトをベースにしてみる。
相変わらずbun使うことにする。(だって早いし)
git clone https://gitlab.com/mt.southern/i18n-next.git drizzle-app
cd drizzle-app
bun install
つらつら、パッケージとか取りに行くので
401 packages installed [15.41s]
とか出たら、次に
bun run dev
でとりあえず動くか確認。確認できたらいよいよdrizzleのセットアップ、とその前にDBはpostgreSQLでいきます。
ただ、ローカルにセットアップするのも億劫なので、supabaseを使うことにする。
こっちのセットアップ等は既に終わっているので割愛。
ではでは、Drizzle ORMのセットアップを行う。ついでにdrizzle-zodとかも入れてみる。
bun add drizzle-orm drizzle-zod postgres
bun add -D drizzle-kit @types/pg
まずはテーブル定義用のファイルを作成する。
mkdir db
touch db/schema.ts
db/schema.ts
import { pgTable, uuid, text, varchar, timestamp } from "drizzle-orm/pg-core";
export const profile = pgTable('profiles', {
id: uuid('id').defaultRandom().primaryKey().notNull(),
fullName: text('full_name'),
phone: varchar('phone', { length: 256 }),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow()
});
続いて、.env.localファイルと接続用のデータベースオブジェクトを作る
touch db/index.ts
touch .env.local
DB_URLはsupabaseの
Dashboard > Settings > Database
のConnection StringのURIからコピペする。
passwordはsupabaseのプロジェクト作成時に設定したもの。
DB_URL=postgresql://postgres:[YOUR-PASSWORD]@db.[project-id].supabase.co:5432/postgres
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";
import * as schema from "@/db/schema";
const connectionString = process.env.DB_URL!;
const client = postgres(connectionString, { max: 1 });
export const db = drizzle(client, { schema });
export default db;
awaitをトップレベルでやると怒られたのでtsconfig.jsonのtargetを"es5"から"ESNext"に変更。
{
"compilerOptions": {
"target": "ESNext",
"lib": [
次は drizzle.config.tsを作成。
それから、drizzleのmigrationファイルとかを格納するディレクトリを作成。
touch drizzle.config.ts
mkdir drizzle
import type { Config } from "drizzle-kit";
export default {
schema: "./db/schema.ts",
out: "./drizzle",
driver: "pg",
dbCredentials: {
connectionString: process.env.DB_URL!,
},
} satisfies Config;
あ、ここでdriverにpgを指定しているのでもってこないとね〜。
bun add pg
主な設定を終わったので後は、マイグレーションコマンドをpackage.jsonにscriptとして登録するだけ。
{
"name": "i18n-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"migrate": "drizzle-kit generate:pg --config=drizzle.config.ts",
"intropect": "drizzle-kit intropect:pg --config=drizzle.config.ts",
"push": "drizzle-kit push:pg --config=drizzle.config.ts",
"drop": "drizzle-kit drop --config=drizzle.config.ts"
},
早速、マイグレーションコマンドを叩く。
bun run migrate
とすると、こんな感じの実行結果となる。
app@59fbc33877b2:/app/drizzle-app$ bun run migrate
$ drizzle-kit generate:pg --config=drizzle.config.ts
drizzle-kit: v0.20.2
drizzle-orm: v0.29.0
Reading config file '/app/drizzle-app/drizzle.config.ts'
1 tables
profiles 7 columns 0 indexes 0 fks
[✓] Your SQL migration file ➜ drizzle/0000_complete_nightmare.sql 🚀
このあと
bun run push
とすると
app@59fbc33877b2:/app/drizzle-app$ bun run push
$ drizzle-kit push:pg --config=drizzle.config.ts
drizzle-kit: v0.20.2
drizzle-orm: v0.29.0
Custom config path was provided, using 'drizzle.config.ts'
Reading config file '/app/drizzle-app/drizzle.config.ts'
[✓] Changes applied
となったので、supabase 覗くとこうなってた。
よしよし、ちゃんとできてるね。
てことで、実際にテーブルにデータをインサートして確認してみる。
app/[lng]/page.tsx
修正
import React from "react";
import Link from "next/link";
import { useTranslation } from "./i18n";
import { profile } from "@/db/schema";
import db from "@/db";
import { eq } from "drizzle-orm";
type NewProfile = typeof profile.$inferInsert;
const addProfile: NewProfile = {
name: "Alice",
email: "alice@example.com",
password: "pass",
role: "ADMIN",
};
const insertProfile = async (p: NewProfile) => {
const data = db
.select()
.from(profile)
.where(eq(profile.email, p.email));
if (data) return;
return db.insert(profile).values(p).returning();
};
const HomePage = async ({ params }: { params: { lng: string } }) => {
const { t } = await useTranslation(params.lng);
const created = await insertProfile(addProfile);
console.log(created);
return (
<>
<h1>{t("home.title")}</h1>
<Link href={`/${params.lng}/about`}>{t("home.link.about")}</Link>
</>
);
};
export default HomePage;
ブラウザからアクセスしてみたら、
[
{
id: 'db6eee66-4f9a-4d24-b736-81798c298a88',
name: 'Alice',
email: 'alice@example.com',
password: 'pass',
role: 'ADMIN',
createdAt: 2023-11-17T08:21:46.998Z,
updatedAt: null
}
]
と表示されていてしっかりテーブルにインサートできた!
とりえずDBとの連携はうまくいったので、次はDrizzleのお作法、特に複数テーブルでリレーションとかみてみる。
このへん見た感じだと、SQLに近い形でクエリを書けるだけでなくて、prismaみたいに、db.query.profile.findMany
とかもいけるらしい。んで、結果が入れ子になる様なのもできるっぽい。
const result = await db.query.users.findMany({
with: {
posts: true
},
});
[{
id: 10,
name: "Dan",
posts: [
{
id: 1,
content: "SQL is awesome",
authorId: 10,
},
{
id: 2,
content: "But check relational queries",
authorId: 10,
}
]
}]
でそれには、スキーマ間でリレーションを張ってあげるとできるっぽい。
こんな感じ。
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name'),
});
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
content: text('content'),
authorId: integer('author_id'),
});
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
ここらへん。これは、users:postsが1:n
のパターン。
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
〜
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
relationで関連作る場合は、posts.autherIdにreferences付けへんのなんでかな??
参照整合性付けたくないってだけ?
ふつうは、
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
content: text('content'),
authorId: integer('author_id').references(() => (users.id)),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
content: text('content'),
authorId: integer('author_id')
.references(() => (users.id), , { onDelete: "cascade" }),
});
な感じか。
リレーションシップの補足。対1側(one)は必ず、元テーブルのフィールドと参照先テーブルのフィールドを明記するので良いが、対多側(many)は、エンティティ名しか書いてなかったりする。
公式のこの辺読むと、同一テーブル間に複数の関連があるとどれとどれをつなぐのかわからんようになるので、名前でうまくつなぐ必要がある。
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name'),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
content: text('content'),
authorId: integer('author_id'),
reviewerId: integer('reviewer_id'),
});
この二つのテーブルのリレーションを
export const usersRelations = relations(users, ({ many }) => ({
author: many(posts, { relationName: 'author' }),
reviewer: many(posts, { relationName: 'reviewer' }),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
relationName: 'author',
}),
reviewer: one(users, {
fields: [posts.reviewerId],
references: [users.id],
relationName: 'reviewer',
}),
}));
こんな感じで、
author: many(posts, { relationName: 'author' }),
author: one(users, {
fields: [posts.authorId],
references: [users.id],
relationName: 'author',
}),
relationName
に同じ定義名にしてあげるとこれを元にリレーションを張ることになる。
この設定をして、
const users = await db.query.users.findMany({
with: {
posts: {
with: {
comments: true,
},
},
},
});
とやると、usersの中にpostsが入れ子になり、更にpostsからcommentsが入れ子になる。
(commentsのスキーマとリレーションは定義しているとして)
ではでは、本格的にschema.ts書いてみるかな〜。
その前に、前に作ったテーブルとかschema.tsは破棄して新たに作成する。
bun drop
ってやると自動生成されたsqlファイル名が表示されて、選択待ちになるので前回作成文は削除しとく。
さて、準備も整ったので、schemaファイルに改めて、テーブルの定義を記述する。
同時にdrizzle-zodでのバリデーション定義もしておく。
import { relations } from "drizzle-orm";
import {
pgTable,
bigserial,
text,
varchar,
timestamp,
bigint,
boolean,
} from "drizzle-orm/pg-core";
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
import { z } from "zod";
const validRoles = ["ADMIN", "MANAGER", "USER", ""] as const;
type RoleType = (typeof validRoles)[number];
export const profile = pgTable("profiles", {
id: bigserial("id", { mode: "number" }).primaryKey().notNull(),
name: varchar("name", { length: 100 }).notNull(),
email: varchar("email", { length: 100 }).notNull().unique(),
password: varchar("password", { length: 64 }).notNull(),
role: varchar("role", { length: 50 }).$type<RoleType>().notNull(),
active: boolean("active").default(true),
createdAt: timestamp("created_at", {
withTimezone: true,
mode: "string",
}).defaultNow(),
updatedAt: timestamp("updated_at", {
withTimezone: true,
mode: "string",
}).defaultNow(),
});
const PASSWORD_RULE =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&-_+])[A-Za-z\d@$!%*?&-_+]{8,}$/;
export const insertProfileSchema = createInsertSchema(profile, {
name: z.string().min(1).max(100),
email: z.string().email().max(100),
password: z.string().regex(PASSWORD_RULE).max(50),
role: z.enum(validRoles),
});
export const selectProfileSchema = createSelectSchema(profile, {
name: z.string().min(1).max(100).optional(),
email: z.string().email().max(100).optional(),
password: z.string().regex(PASSWORD_RULE).max(50).optional(),
role: z.enum(validRoles).optional(),
});
drizzle-zodのcreateInsertSchemaやcreateSelectSchemaではデフォルトで最低限のバリデーションルールは付与してくれるが、全然足りないので再定義する。
つぎはサービスモジュールを作る。本来はもう少し分けてDBアクセスはリポジトリとかにレイヤを分けた方がとも思ったけど、めんどいのでいったんここでDBアクセスもしてしまう。
気が向いたらそのうちリファクタするかな。
import db from "@/db";
import { profile, insertProfileSchema } from "@/db/schema";
import { eq } from "drizzle-orm";
export type { Profile, ProfileParam as ProfileInsert };
export {
findAllActiveProfiles
};
type Profile = typeof profile.$inferSelect;
type ProfileParam = typeof profile.$inferInsert;
const findAllActiveProfiles = async (): Promise<Profile[]> => {
const profiles = await db.query.profile.findMany({
where: eq(profile.active, true),
});
return profiles;
};
APIにして、APIから関数呼び出ししてみる。
export const GET = async (req: NextRequest) => {
const profileList: Profile[] = await findAllActiveProfiles();
return NextResponse.json(profileList);
};