Drizzle with Neon Postgres

이 νŠœν† λ¦¬μ–Όμ€ Neon Postgres λ°μ΄ν„°λ² μ΄μŠ€μ™€ Drizzle ORM을 μ‚¬μš©ν•˜λŠ” 방법을 λ³΄μ—¬μ€λ‹ˆλ‹€. Neon 계정이 μ—†λŠ” 경우 μ—¬κΈ°μ—μ„œ κ°€μž…ν•˜μ„Έμš”.

This guide assumes familiarity with:
  • Drizzle ORMκ³Ό Drizzle kit이 μ„€μΉ˜λ˜μ–΄ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€. λ‹€μŒ λͺ…령을 μ‹€ν–‰ν•˜μ—¬ μ„€μΉ˜ν•  수 μžˆμŠ΅λ‹ˆλ‹€:
npm
yarn
pnpm
bun
npm i drizzle-orm
npm i -D drizzle-kit
npm
yarn
pnpm
bun
npm i @neondatabase/serverless
  • ν™˜κ²½ λ³€μˆ˜ 관리λ₯Ό μœ„ν•œ dotenv νŒ¨ν‚€μ§€κ°€ μ„€μΉ˜λ˜μ–΄ μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€.
npm
yarn
pnpm
bun
npm i dotenv

Neon 및 Drizzle ORM μ„€μ •ν•˜κΈ°

μƒˆ Neon ν”„λ‘œμ νŠΈ μƒμ„±ν•˜κΈ°

Neon Console에 λ‘œκ·ΈμΈν•˜μ—¬ Projects μ„Ήμ…˜μœΌλ‘œ μ΄λ™ν•©λ‹ˆλ‹€. ν”„λ‘œμ νŠΈλ₯Ό μ„ νƒν•˜κ±°λ‚˜ New Project λ²„νŠΌμ„ ν΄λ¦­ν•˜μ—¬ μƒˆ ν”„λ‘œμ νŠΈλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.

Neon ν”„λ‘œμ νŠΈμ—λŠ” neondbλΌλŠ” μ΄λ¦„μ˜ λ°”λ‘œ μ‚¬μš© κ°€λŠ₯ν•œ Postgres λ°μ΄ν„°λ² μ΄μŠ€κ°€ ν¬ν•¨λ˜μ–΄ μžˆμŠ΅λ‹ˆλ‹€. 이 νŠœν† λ¦¬μ–Όμ—μ„œλŠ” 이λ₯Ό μ‚¬μš©ν•˜κ² μŠ΅λ‹ˆλ‹€.

μ—°κ²° λ¬Έμžμ—΄ λ³€μˆ˜ μ„€μ •ν•˜κΈ°

ν”„λ‘œμ νŠΈ μ½˜μ†”μ˜ Connection Details μ„Ήμ…˜μœΌλ‘œ μ΄λ™ν•˜μ—¬ λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° λ¬Έμžμ—΄μ„ μ°ΎμŠ΅λ‹ˆλ‹€. λ‹€μŒκ³Ό μœ μ‚¬ν•œ ν˜•νƒœμž…λ‹ˆλ‹€:

postgres://username:password@ep-cool-darkness-123456.us-east-2.aws.neon.tech/neondb

Neon λ°μ΄ν„°λ² μ΄μŠ€μ— μ—°κ²°ν•˜λŠ” 데 μ‚¬μš©ν•  DATABASE_URL ν™˜κ²½ λ³€μˆ˜λ₯Ό .env λ˜λŠ” .env.local νŒŒμΌμ— μΆ”κ°€ν•©λ‹ˆλ‹€.

DATABASE_URL=NEON_DATABASE_CONNECTION_STRING

Drizzle ORM을 λ°μ΄ν„°λ² μ΄μŠ€μ— μ—°κ²°ν•˜κΈ°

db.ts νŒŒμΌμ„ μƒμ„±ν•˜κ³  λ°μ΄ν„°λ² μ΄μŠ€ 섀정을 κ΅¬μ„±ν•©λ‹ˆλ‹€:

src/db.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import { config } from "dotenv";

config({ path: ".env" }); // or .env.local

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle({ client: sql });

ν…Œμ΄λΈ” μƒμ„±ν•˜κΈ°

schema.ts νŒŒμΌμ„ μƒμ„±ν•˜κ³  ν…Œμ΄λΈ”μ„ μ„ μ–Έν•©λ‹ˆλ‹€:

src/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 νŒŒμΌμ„ μƒμ„±ν•˜κ³  λ‹€μŒ λ‚΄μš©μ„ μΆ”κ°€ν•©λ‹ˆλ‹€:

drizzle.config.ts
import { config } from 'dotenv';
import { defineConfig } from "drizzle-kit";

config({ path: '.env' });

export default defineConfig({
  schema: "./src/schema.ts",
  out: "./migrations",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

λ°μ΄ν„°λ² μ΄μŠ€μ— 변경사항 μ μš©ν•˜κΈ°

drizzle-kit generate λͺ…령을 μ‚¬μš©ν•˜μ—¬ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜μ„ μƒμ„±ν•œ λ‹€μŒ drizzle-kit migrate λͺ…λ ΉμœΌλ‘œ μ‹€ν–‰ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 생성:

npx drizzle-kit generate

μ΄λŸ¬ν•œ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜μ€ drizzle.config.ts에 μ§€μ •λœ λŒ€λ‘œ drizzle/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 "public"."users_table"("id") ON DELETE cascade ON UPDATE no action;
EXCEPTION
 WHEN duplicate_object THEN null;
END $$;

λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ‹€ν–‰:

npx drizzle-kit migrate

λ˜λŠ” Drizzle kit push λͺ…령을 μ‚¬μš©ν•˜μ—¬ 변경사항을 λ°μ΄ν„°λ² μ΄μŠ€μ— 직접 ν‘Έμ‹œν•  수 μžˆμŠ΅λ‹ˆλ‹€:

npx drizzle-kit push
IMPORTANT
Push λͺ…령은 둜컬 개발 ν™˜κ²½μ—μ„œ μƒˆλ‘œμš΄ μŠ€ν‚€λ§ˆ μ„€κ³„λ‚˜ 변경사항을 λΉ λ₯΄κ²Œ ν…ŒμŠ€νŠΈν•΄μ•Ό ν•˜λŠ” κ²½μš°μ— μ ν•©ν•˜λ©°, λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ 파일 κ΄€λ¦¬μ˜ λΆ€λ‹΄ 없이 λΉ λ₯Έ 반볡이 κ°€λŠ₯ν•©λ‹ˆλ‹€.

기본 파일 ꡬ쑰

λ‹€μŒμ€ ν”„λ‘œμ νŠΈμ˜ κΈ°λ³Έ 파일 κ΅¬μ‘°μž…λ‹ˆλ‹€. src/db λ””λ ‰ν„°λ¦¬μ—λŠ” db.ts의 μ—°κ²°, schema.ts의 μŠ€ν‚€λ§ˆ μ •μ˜λ₯Ό ν¬ν•¨ν•œ λ°μ΄ν„°λ² μ΄μŠ€ κ΄€λ ¨ 파일이 있으며, migrations 디렉터리에 μ €μž₯된 λ§ˆμ΄κ·Έλ ˆμ΄μ…˜μ„ μ μš©ν•˜λŠ” 역할을 ν•˜λŠ” migrate.ts 파일이 μžˆμŠ΅λ‹ˆλ‹€.

πŸ“¦ <project root>
 β”œ πŸ“‚ src
 β”‚  β”œ πŸ“œ db.ts
 β”‚  β”” πŸ“œ schema.ts
 β”œ πŸ“‚ migrations
 β”‚  β”œ πŸ“‚ meta
 β”‚  β”‚  β”œ πŸ“œ _journal.json
 β”‚  β”‚  β”” πŸ“œ 0000_snapshot.json
 β”‚  β”” πŸ“œ 0000_dry_richard_fisk.sql
 β”œ πŸ“œ .env
 β”œ πŸ“œ drizzle.config.ts
 β”œ πŸ“œ package.json
 β”” πŸ“œ tsconfig.json

쿼리 μ˜ˆμ‹œ

예λ₯Ό λ“€μ–΄, src/queries 폴더λ₯Ό μƒμ„±ν•˜κ³  각 μž‘μ—…μ— λŒ€ν•΄ λ³„λ„μ˜ νŒŒμΌμ„ λ§Œλ“­λ‹ˆλ‹€: insert, select, update, delete.

데이터 μ‚½μž…

insert 쿼리에 λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ λ¬Έμ„œλ₯Ό μ°Έμ‘°ν•˜μ„Έμš”.

src/queries/insert.ts
import { db } from '../db';
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 쿼리에 λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ λ¬Έμ„œλ₯Ό μ°Έμ‘°ν•˜μ„Έμš”.

src/queries/select.ts
import { asc, between, count, eq, getTableColumns, sql } from 'drizzle-orm';
import { db } from '../db';
import { SelectUser, usersTable, postsTable } 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 쿼리에 λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ λ¬Έμ„œλ₯Ό μ°Έμ‘°ν•˜μ„Έμš”.

src/queries/update.ts
import { eq } from 'drizzle-orm';
import { db } from '../db';
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 쿼리에 λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ λ¬Έμ„œλ₯Ό μ°Έμ‘°ν•˜μ„Έμš”.

src/queries/delete.ts
import { db } from '../db';
import { eq } from 'drizzle-orm';
import { SelectUser, usersTable } from '../schema';

export async function deleteUser(id: SelectUser['id']) {
  await db.delete(usersTable).where(eq(usersTable.id, id));
}