Drizzle with Supabase Database
μ΄ νν 리μΌμ Supabase Databaseμ Drizzle ORMμ μ¬μ©νλ λ°©λ²μ 보μ¬μ€λλ€. λͺ¨λ Supabase νλ‘μ νΈμλ μμ ν Postgres λ°μ΄ν°λ² μ΄μ€κ° ν¬ν¨λμ΄ μμ΅λλ€.
- Drizzle ORMκ³Ό Drizzle kitμ΄ μ€μΉλμ΄ μμ΄μΌ ν©λλ€. λ€μ λͺ λ Ήμ μ€ννμ¬ μ€μΉν μ μμ΅λλ€:
npm i drizzle-orm
npm i -D drizzle-kit
- νκ²½ λ³μ κ΄λ¦¬λ₯Ό μν
dotenvν¨ν€μ§κ° μ€μΉλμ΄ μμ΄μΌ ν©λλ€. μ΄ ν¨ν€μ§μ λν μμΈν λ΄μ©μ μ¬κΈ°λ₯Ό μ°Έμ‘°νμΈμ.
npm i dotenv
- Postgres λ°μ΄ν°λ² μ΄μ€μ μ°κ²°νκΈ° μν
postgresν¨ν€μ§κ° μ€μΉλμ΄ μμ΄μΌ ν©λλ€. μ΄ ν¨ν€μ§μ λν μμΈν λ΄μ©μ μ¬κΈ°λ₯Ό μ°Έμ‘°νμΈμ.
npm i postgres
- μ΅μ λ²μ μ Supabase CLIκ° μ€μΉλμ΄ μμ΄μΌ ν©λλ€ (Supabase CLIλ₯Ό λ§μ΄κ·Έλ μ΄μ μ μ¬μ©νλ €λ κ²½μ°μλ§ νμ)
Drizzle ORMμΌλ‘ λ°μ΄ν°λ² μ΄μ€μ μ°κ²°νλ λ°©λ²μ μμλ³΄λ €λ©΄ Supabase λ¬Έμλ₯Ό νμΈνμΈμ.
Supabase λ° Drizzle ORM μ€μ νκΈ°
μ Supabase νλ‘μ νΈ μμ±νκΈ°
λμ보λμμ λλ μ΄ λ§ν¬λ₯Ό λ°λΌ μ Supabase νλ‘μ νΈλ₯Ό μμ±ν μ μμ΅λλ€.
μ°κ²° λ¬Έμμ΄ λ³μ μ€μ νκΈ°
Database Settingsλ‘ μ΄λνμ¬ Connection String μΉμ
μμ URIλ₯Ό 볡μ¬ν©λλ€. connection poolingμ μ¬μ©ν΄μΌ ν©λλ€. λΉλ°λ²νΈ νλ μ΄μ€νλλ₯Ό μ€μ λ°μ΄ν°λ² μ΄μ€ λΉλ°λ²νΈλ‘ λ°κΏμΌ ν©λλ€.
.env λλ .env.local νμΌμ DATABASE_URL λ³μλ₯Ό μΆκ°ν©λλ€.
DATABASE_URL=<YOUR_DATABASE_URL>Connection Pooler λ° νλ§ λͺ¨λμ λν μμΈν λ΄μ©μ λ¬Έμλ₯Ό μ°Έμ‘°νμΈμ.
Drizzle ORMμ λ°μ΄ν°λ² μ΄μ€μ μ°κ²°νκΈ°
src/db λλ ν°λ¦¬μ index.ts νμΌμ μμ±νκ³ λ°μ΄ν°λ² μ΄μ€ μ€μ μ ꡬμ±ν©λλ€:
import { config } from 'dotenv';
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
config({ path: '.env' }); // or .env.local
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle({ client });ν μ΄λΈ μμ±νκΈ°
src/db λλ ν°λ¦¬μ schema.ts νμΌμ μμ±νκ³ ν
μ΄λΈμ μ μΈν©λλ€:
import { integer, pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
export const usersTable = pgTable('users_table', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
age: integer('age').notNull(),
email: text('email').notNull().unique(),
});
export const postsTable = pgTable('posts_table', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
userId: integer('user_id')
.notNull()
.references(() => usersTable.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at')
.notNull()
.$onUpdate(() => new Date()),
});
export type InsertUser = typeof usersTable.$inferInsert;
export type SelectUser = typeof usersTable.$inferSelect;
export type InsertPost = typeof postsTable.$inferInsert;
export type SelectPost = typeof postsTable.$inferSelect;Drizzle μ€μ νμΌ κ΅¬μ±νκΈ°
Drizzle config - Drizzle Kitμμ μ¬μ©λλ μ€μ νμΌλ‘, λ°μ΄ν°λ² μ΄μ€ μ°κ²°, λ§μ΄κ·Έλ μ΄μ ν΄λ λ° μ€ν€λ§ νμΌμ λν λͺ¨λ μ 보λ₯Ό ν¬ν¨ν©λλ€.
νλ‘μ νΈ λ£¨νΈμ drizzle.config.ts νμΌμ μμ±νκ³ λ€μ λ΄μ©μ μΆκ°ν©λλ€:
import { config } from 'dotenv';
import { defineConfig } from 'drizzle-kit';
config({ path: '.env' });
export default defineConfig({
schema: './src/db/schema.ts',
out: './supabase/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});λ°μ΄ν°λ² μ΄μ€μ λ³κ²½μ¬ν μ μ©νκΈ°
drizzle-kit generate λͺ
λ Ήμ μ¬μ©νμ¬ λ§μ΄κ·Έλ μ΄μ
μ μμ±ν λ€μ drizzle-kit migrate λͺ
λ ΉμΌλ‘ μ€νν μ μμ΅λλ€.
λ§μ΄κ·Έλ μ΄μ μμ±:
npx drizzle-kit generateμ΄λ¬ν λ§μ΄κ·Έλ μ΄μ
μ drizzle.config.tsμ μ§μ λ λλ‘ supabase/migrations λλ ν°λ¦¬μ μ μ₯λ©λλ€. μ΄ λλ ν°λ¦¬μλ λ°μ΄ν°λ² μ΄μ€ μ€ν€λ§λ₯Ό μ
λ°μ΄νΈνλ λ° νμν SQL νμΌκ³Ό λ€μν λ§μ΄κ·Έλ μ΄μ
λ¨κ³μμ μ€ν€λ§μ μ€λ
μ·μ μ μ₯νλ meta ν΄λκ° ν¬ν¨λ©λλ€.
μμ±λ λ§μ΄κ·Έλ μ΄μ μμ:
CREATE TABLE IF NOT EXISTS "posts_table" (
"id" serial PRIMARY KEY NOT NULL,
"title" text NOT NULL,
"content" text NOT NULL,
"user_id" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "users_table" (
"id" serial PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"age" integer NOT NULL,
"email" text NOT NULL,
CONSTRAINT "users_table_email_unique" UNIQUE("email")
);
--> statement-breakpoint
DO $$ BEGIN
ALTER TABLE "posts_table" ADD CONSTRAINT "posts_table_user_id_users_table_id_fk" FOREIGN KEY ("user_id") REFERENCES "users_table"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;λ§μ΄κ·Έλ μ΄μ μ€ν:
npx drizzle-kit migrateλ§μ΄κ·Έλ μ΄μ νλ‘μΈμ€μ λν΄ μμΈν μμ보μΈμ. Supabase CLIλ₯Ό μ¬μ©νμ¬ λ§μ΄κ·Έλ μ΄μ μ μ μ©ν μλ μμ΅λλ€:
- μ΄λ―Έ μ‘΄μ¬νλ ν
μ΄λΈμ κ²½μ°,
npx drizzle-kit generateλ‘ μμ±λ λ§μ΄κ·Έλ μ΄μ νμΌμ μλμΌλ‘ κ²ν νκ³ μμ νμ§ μμ μμ μμ± κ΅¬λ¬Έ(μ:CREATE SCHEMA "auth";)μ μ£Όμ μ²λ¦¬νκ±°λ μ‘°μ νλ, μμ ν μ‘°κ±΄λΆ μμ±(μ:CREATE TABLE IF NOT EXISTS "auth"."users")μ΄ μ¬λ°λ₯΄κ² μ²λ¦¬λλλ‘ ν©λλ€.
λλ Drizzle kit push λͺ λ Ήμ μ¬μ©νμ¬ λ³κ²½μ¬νμ λ°μ΄ν°λ² μ΄μ€μ μ§μ νΈμν μ μμ΅λλ€:
npx drizzle-kit pushSupabase CLIλ₯Ό μ¬μ©νμ¬ λ§μ΄κ·Έλ μ΄μ μ μ μ©νλ €λ©΄ λ€μ λ¨κ³λ₯Ό λ°λΌμΌ ν©λλ€:
Drizzle Kitμ μ¬μ©νμ¬ λ§μ΄κ·Έλ μ΄μ μμ±:
npx drizzle-kit generateλ‘컬 Supabase νλ‘μ νΈ μ΄κΈ°ν:
supabase initμ격 νλ‘μ νΈμ μ°κ²°:
supabase linkλ°μ΄ν°λ² μ΄μ€μ λ³κ²½μ¬ν νΈμ:
supabase db pushκΈ°λ³Έ νμΌ κ΅¬μ‘°
λ€μμ νλ‘μ νΈμ κΈ°λ³Έ νμΌ κ΅¬μ‘°μ
λλ€. src/db λλ ν°λ¦¬μλ index.tsμ μ°κ²°κ³Ό schema.tsμ μ€ν€λ§ μ μλ₯Ό ν¬ν¨ν λ°μ΄ν°λ² μ΄μ€ κ΄λ ¨ νμΌμ΄ μμ΅λλ€.
π¦ <project root>
β π src
β β π db
β β β π index.ts
β β β π schema.ts
β π supabase
β β π migrations
β β β π meta
β β β β π _journal.json
β β β β π 0000_snapshot.json
β β β π 0000_watery_spencer_smythe.sql
β β π config.toml
β π .env
β π drizzle.config.ts
β π package.json
β π tsconfig.json쿼리 μμ
μλ₯Ό λ€μ΄, src/db/queries ν΄λλ₯Ό μμ±νκ³ κ° μμ
μ λν΄ λ³λμ νμΌμ λ§λλλ€: insert, select, update, delete.
λ°μ΄ν° μ½μ
insert 쿼리μ λν μμΈν λ΄μ©μ λ¬Έμλ₯Ό μ°Έμ‘°νμΈμ.
import { db } from '../index';
import { InsertPost, InsertUser, postsTable, usersTable } from '../schema';
export async function createUser(data: InsertUser) {
await db.insert(usersTable).values(data);
}
export async function createPost(data: InsertPost) {
await db.insert(postsTable).values(data);
}λ°μ΄ν° μ‘°ν
select 쿼리μ λν μμΈν λ΄μ©μ λ¬Έμλ₯Ό μ°Έμ‘°νμΈμ.
import { asc, between, count, eq, getTableColumns, sql } from 'drizzle-orm';
import { db } from '../index';
import { SelectUser, postsTable, usersTable } from '../schema';
export async function getUserById(id: SelectUser['id']): Promise<
Array<{
id: number;
name: string;
age: number;
email: string;
}>
> {
return db.select().from(usersTable).where(eq(usersTable.id, id));
}
export async function getUsersWithPostsCount(
page = 1,
pageSize = 5,
): Promise<
Array<{
postsCount: number;
id: number;
name: string;
age: number;
email: string;
}>
> {
return db
.select({
...getTableColumns(usersTable),
postsCount: count(postsTable.id),
})
.from(usersTable)
.leftJoin(postsTable, eq(usersTable.id, postsTable.userId))
.groupBy(usersTable.id)
.orderBy(asc(usersTable.id))
.limit(pageSize)
.offset((page - 1) * pageSize);
}
export async function getPostsForLast24Hours(
page = 1,
pageSize = 5,
): Promise<
Array<{
id: number;
title: string;
}>
> {
return db
.select({
id: postsTable.id,
title: postsTable.title,
})
.from(postsTable)
.where(between(postsTable.createdAt, sql`now() - interval '1 day'`, sql`now()`))
.orderBy(asc(postsTable.title), asc(postsTable.id))
.limit(pageSize)
.offset((page - 1) * pageSize);
}λλ κ΄κ³ν 쿼리 ꡬ문μ μ¬μ©ν μλ μμ΅λλ€.
λ°μ΄ν° μ λ°μ΄νΈ
update 쿼리μ λν μμΈν λ΄μ©μ λ¬Έμλ₯Ό μ°Έμ‘°νμΈμ.
import { eq } from 'drizzle-orm';
import { db } from '../index';
import { SelectPost, postsTable } from '../schema';
export async function updatePost(id: SelectPost['id'], data: Partial<Omit<SelectPost, 'id'>>) {
await db.update(postsTable).set(data).where(eq(postsTable.id, id));
}λ°μ΄ν° μμ
delete 쿼리μ λν μμΈν λ΄μ©μ λ¬Έμλ₯Ό μ°Έμ‘°νμΈμ.
import { eq } from 'drizzle-orm';
import { db } from '../index';
import { SelectUser, usersTable } from '../schema';
export async function deleteUser(id: SelectUser['id']) {
await db.delete(usersTable).where(eq(usersTable.id, id));
}