Drizzle과 SQLite Durable Objects 시작하기

This guide assumes familiarity with:
  • dotenv - 환경 변수 관리 패키지 - 자세히 보기
  • tsx - TypeScript 파일 실행 패키지 - 자세히 보기
  • Cloudflare SQLite Durable Objects - Durable Object 내에 내장된 SQLite 데이터베이스 - 자세히 보기
  • wrangler - Cloudflare Developer Platform 커맨드라인 인터페이스 - 자세히 보기

Basic file structure

This is the basic file structure of the project. In the src/db directory, we have table definition in schema.ts. In drizzle folder there are sql migration file and snapshots.

📦 <project root>
 ├ 📂 drizzle
 ├ 📂 src
 │   ├ 📂 db
 │   │  └ 📜 schema.ts
 │   └ 📜 index.ts
 ├ 📜 .env
 ├ 📜 drizzle.config.ts
 ├ 📜 package.json
 └ 📜 tsconfig.json

1단계 - 필수 패키지 설치

npm
yarn
pnpm
bun
npm i drizzle-orm wrangler dotenv
npm i -D drizzle-kit tsx

2단계 - wrangler.toml 설정

D1 데이터베이스를 위한 wrangler.toml 파일이 필요하며 다음과 같은 형태입니다:

#:schema node_modules/wrangler/config-schema.json
name = "sqlite-durable-objects"
main = "src/index.ts"
compatibility_date = "2024-11-12"
compatibility_flags = [ "nodejs_compat" ]

# Bind a Durable Object. Durable objects are a scale-to-zero compute primitive based on the actor model.
# Durable Objects can live for as long as needed. Use these when you need a long-running "server", such as in realtime apps.
# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects
[[durable_objects.bindings]]
name = "MY_DURABLE_OBJECT"
class_name = "MyDurableObject"

# Durable Object migrations.
# Docs: https://developers.cloudflare.com/workers/wrangler/configuration/#migrations
[[migrations]]
tag = "v1"
new_sqlite_classes = ["MyDurableObject"]

# We need rules so we can import migrations in the next steps
[[rules]] 
type = "Text"
globs = ["**/*.sql"]
fallthrough = true

3단계 - Drizzle ORM을 데이터베이스에 연결

import { drizzle, type DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite';
import { DurableObject } from 'cloudflare:workers'

export class MyDurableObject extends DurableObject {
	storage: DurableObjectStorage;
	db: DrizzleSqliteDODatabase;

	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		this.storage = ctx.storage;
		this.db = drizzle(this.storage, { logger: false });
	}
}

4단계 - wrangler 타입 생성

npm
yarn
pnpm
bun
npx wrangler types

이 명령어의 출력 결과는 worker-configuration.d.ts 파일입니다.

5단계 - 테이블 생성

Create a schema.ts file in the src/db directory and declare your table:

src/db/schema.ts
import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";

export const usersTable = sqliteTable("users_table", {
  id: int().primaryKey({ autoIncrement: true }),
  name: text().notNull(),
  age: int().notNull(),
  email: text().notNull().unique(),
});

6단계 - Drizzle 설정 파일 구성

Drizzle config - Drizzle Kit에서 사용하는 설정 파일이며, 데이터베이스 연결, 마이그레이션 폴더 및 스키마 파일에 대한 모든 정보를 포함합니다.

프로젝트 루트에 drizzle.config.ts 파일을 생성하고 다음 내용을 추가합니다:

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

export default defineConfig({
  out: './drizzle',
  schema: './src/db/schema.ts',
  dialect: 'sqlite',
  driver: 'durable-sqlite',
});

7단계 - 데이터베이스에 변경사항 적용

마이그레이션 생성:

npx drizzle-kit generate

마이그레이션은 Cloudflare Workers에서만 적용할 수 있습니다. 이를 위해 MyDurableObject에 마이그레이션 기능을 정의합니다:

import { drizzle, type DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite';
import { DurableObject } from 'cloudflare:workers'
import { migrate } from 'drizzle-orm/durable-sqlite/migrator';
import migrations from '../drizzle/migrations';

export class MyDurableObject extends DurableObject {
	storage: DurableObjectStorage;
	db: DrizzleSqliteDODatabase;

	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		this.storage = ctx.storage;
		this.db = drizzle(this.storage, { logger: false });
	}

	async migrate() {
		migrate(this.db, migrations);
	}
}

8단계 - 데이터베이스 마이그레이션 및 쿼리

import { drizzle, DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite';
import { DurableObject } from 'cloudflare:workers'
import { migrate } from 'drizzle-orm/durable-sqlite/migrator';
import migrations from '../drizzle/migrations';
import { usersTable } from './db/schema';

export class MyDurableObject extends DurableObject {
	storage: DurableObjectStorage;
	db: DrizzleSqliteDODatabase<any>;

	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		this.storage = ctx.storage;
		this.db = drizzle(this.storage, { logger: false });

		// 쿼리를 수락하기 전에 모든 마이그레이션이 완료되었는지 확인합니다.
		// 그렇지 않으면 Drizzle 데이터베이스 `this.db`에 접근하는 모든 함수에서
		// `this.migrate()`를 실행해야 합니다.
		ctx.blockConcurrencyWhile(async () => {
			await this._migrate();
		});
	}

	async insertAndList(user: typeof usersTable.$inferInsert) {
		await this.insert(user);
		return this.select();
	}

	async insert(user: typeof usersTable.$inferInsert) {
		await this.db.insert(usersTable).values(user);
	}

	async select() {
		return this.db.select().from(usersTable);
	}

	async _migrate() {
		migrate(this.db, migrations);
	}
}

export default {
	/**
	 * Cloudflare Worker의 표준 fetch 핸들러
	 *
	 * @param request - 클라이언트에서 Worker로 제출된 요청
	 * @param env - wrangler.toml에 선언된 바인딩을 참조하는 인터페이스
	 * @param ctx - Worker의 실행 컨텍스트
	 * @returns 클라이언트에 전송될 응답
	 */
	async fetch(request: Request, env: Env): Promise<Response> {
		const id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName('durable-object');
		const stub = env.MY_DURABLE_OBJECT.get(id);

		// 옵션 A - 최대 성능.
		// DO 내에서 데이터베이스 접근이 빠르기 때문에,
		// 모든 데이터베이스 상호작용을 하나의 Durable Object 호출로 묶는 것이 최대 성능을 위해 권장됩니다.
		const usersAll = await stub.insertAndList({
			name: 'John',
			age: 30,
			email: 'john@example.com',
		});
		console.log('New user created. Getting all users from the database: ', users);

		// 옵션 B - 느리지만 디버깅에 유용할 수 있습니다.
		// 노출된 개별 Drizzle 쿼리를 직접 호출할 수도 있지만,
		// 모든 쿼리가 Durable Object 인스턴스로의 왕복임을 유의하세요.
		await stub.insert({
			name: 'John',
			age: 30,
			email: 'john@example.com',
		});
		console.log('New user created!');

		const users = await stub.select();
		console.log('Getting all users from the database: ', users);

		return Response.json(users);
	}
}