Drizzle with Turso

이 νŠœν† λ¦¬μ–Όμ€ Turso와 Drizzle ORM을 μ‚¬μš©ν•˜λŠ” 방법을 λ³΄μ—¬μ€λ‹ˆλ‹€.

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

TursoλŠ” SQLite의 Open Contribution 포크인 libSQL을 기반으둜 κ΅¬μΆ•λœ SQLite ν˜Έν™˜ λ°μ΄ν„°λ² μ΄μŠ€μž…λ‹ˆλ‹€. 쑰직당 μˆ˜μ‹­λ§Œ 개의 λ°μ΄ν„°λ² μ΄μŠ€λ‘œ ν™•μž₯ν•  수 있으며, 마이크둜초 μ§€μ—° μ‹œκ°„ μ•‘μ„ΈμŠ€λ₯Ό μœ„ν•΄ 자체 μ„œλ²„λ₯Ό ν¬ν•¨ν•œ λͺ¨λ“  μœ„μΉ˜λ‘œμ˜ 볡제λ₯Ό μ§€μ›ν•©λ‹ˆλ‹€. Turso의 κ°œλ…μ— λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ μ—¬κΈ°μ—μ„œ 읽을 수 μžˆμŠ΅λ‹ˆλ‹€.

Drizzle ORM은 libSQL λ“œλΌμ΄λ²„λ₯Ό 기본적으둜 μ§€μ›ν•©λ‹ˆλ‹€. μš°λ¦¬λŠ” SQL λ°©μ–Έκ³Ό 방언별 λ“œλΌμ΄λ²„ 및 ꡬ문을 μˆ˜μš©ν•˜λ©°, κ°€μž₯ 인기 μžˆλŠ” SQLite와 μœ μ‚¬ν•œ all, get, values, run 쿼리 λ©”μ„œλ“œ ꡬ문을 λ―ΈλŸ¬λ§ν•©λ‹ˆλ‹€.

Turso λ°μ΄ν„°λ² μ΄μŠ€ 섀정에 λŒ€ν•΄μ„œλŠ” 곡식 λ¬Έμ„œλ₯Ό ν™•μΈν•˜μ„Έμš”.

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

Turso에 κ°€μž… λ˜λŠ” λ‘œκ·ΈμΈν•˜κΈ°

κ°€μž…:

turso auth signup

둜그인:

turso auth login

μƒˆ λ°μ΄ν„°λ² μ΄μŠ€ μƒμ„±ν•˜κΈ°

turso db create <DATABASE_NAME> λͺ…령을 μ‹€ν–‰ν•˜μ—¬ μƒˆ λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μƒμ„±ν•©λ‹ˆλ‹€:

turso db create drizzle-turso-db

λ°μ΄ν„°λ² μ΄μŠ€μ— λŒ€ν•œ 정보λ₯Ό 보렀면 λ‹€μŒ λͺ…령을 μ‹€ν–‰ν•©λ‹ˆλ‹€:

turso db show drizzle-turso-db

인증 토큰 μƒμ„±ν•˜κΈ°

λ°μ΄ν„°λ² μ΄μŠ€μ— λŒ€ν•œ 인증 토큰을 μƒμ„±ν•˜λ €λ©΄ λ‹€μŒ λͺ…령을 μ‹€ν–‰ν•©λ‹ˆλ‹€:

turso db tokens create drizzle-turso-db

이 λͺ…λ Ή 및 μ˜΅μ…˜μ— λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ λ¬Έμ„œλ₯Ό μ°Έμ‘°ν•˜μ„Έμš”.

ν™˜κ²½ λ³€μˆ˜ μ—…λ°μ΄νŠΈν•˜κΈ°

μ—°κ²° URLκ³Ό 인증 ν† ν°μœΌλ‘œ .env λ˜λŠ” .env.local νŒŒμΌμ„ μ—…λ°μ΄νŠΈν•©λ‹ˆλ‹€.

TURSO_CONNECTION_URL=
TURSO_AUTH_TOKEN=

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

src/db 디렉터리에 index.ts νŒŒμΌμ„ μƒμ„±ν•˜κ³  λ°μ΄ν„°λ² μ΄μŠ€ 섀정을 κ΅¬μ„±ν•©λ‹ˆλ‹€:

src/db/index.ts
import { config } from 'dotenv';
import { drizzle } from 'drizzle-orm/libsql';

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

export const db = drizzle({ connection: {
  url: process.env.TURSO_CONNECTION_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
}});

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

src/db 디렉터리에 schema.ts νŒŒμΌμ„ μƒμ„±ν•˜κ³  ν…Œμ΄λΈ”μ„ μ„ μ–Έν•©λ‹ˆλ‹€:

src/db/schema.ts
import { sql } from 'drizzle-orm';
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';

export const usersTable = sqliteTable('users', {
  id: integer('id').primaryKey(),
  name: text('name').notNull(),
  age: integer('age').notNull(),
  email: text('email').unique().notNull(),
});

export const postsTable = sqliteTable('posts', {
  id: integer('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content').notNull(),
  userId: integer('user_id')
    .notNull()
    .references(() => usersTable.id, { onDelete: 'cascade' }),
  createdAt: text('created_at')
    .default(sql`(CURRENT_TIMESTAMP)`)
    .notNull(),
  updatedAt: integer('updated_at', { mode: 'timestamp' }).$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/db/schema.ts',
  out: './migrations',
  dialect: 'turso',
  dbCredentials: {
    url: process.env.TURSO_CONNECTION_URL!,
    authToken: process.env.TURSO_AUTH_TOKEN!,
  },
});

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

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

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

npx drizzle-kit generate

μ΄λŸ¬ν•œ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜μ€ drizzle.config.ts에 μ§€μ •λœ λŒ€λ‘œ migrations 디렉터리에 μ €μž₯λ©λ‹ˆλ‹€. 이 λ””λ ‰ν„°λ¦¬μ—λŠ” λ°μ΄ν„°λ² μ΄μŠ€ μŠ€ν‚€λ§ˆλ₯Ό μ—…λ°μ΄νŠΈν•˜λŠ” 데 ν•„μš”ν•œ SQL 파일과 λ‹€μ–‘ν•œ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ λ‹¨κ³„μ—μ„œ μŠ€ν‚€λ§ˆμ˜ μŠ€λƒ…μƒ·μ„ μ €μž₯ν•˜λŠ” meta 폴더가 ν¬ν•¨λ©λ‹ˆλ‹€.

μƒμ„±λœ λ§ˆμ΄κ·Έλ ˆμ΄μ…˜ μ˜ˆμ‹œ:

CREATE TABLE `posts` (
	`id` integer PRIMARY KEY NOT NULL,
	`title` text NOT NULL,
	`content` text NOT NULL,
	`user_id` integer NOT NULL,
	`created_at` text DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
	`updated_at` integer,
	FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `users` (
	`id` integer PRIMARY KEY NOT NULL,
	`name` text NOT NULL,
	`age` integer NOT NULL,
	`email` text NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);

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

npx drizzle-kit migrate

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

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

기본 파일 ꡬ쑰

λ‹€μŒμ€ ν”„λ‘œμ νŠΈμ˜ κΈ°λ³Έ 파일 κ΅¬μ‘°μž…λ‹ˆλ‹€. src/db λ””λ ‰ν„°λ¦¬μ—λŠ” index.ts의 μ—°κ²°κ³Ό schema.ts의 μŠ€ν‚€λ§ˆ μ •μ˜λ₯Ό ν¬ν•¨ν•œ λ°μ΄ν„°λ² μ΄μŠ€ κ΄€λ ¨ 파일이 μžˆμŠ΅λ‹ˆλ‹€.

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

쿼리 μ˜ˆμ‹œ

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

데이터 μ‚½μž…

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

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

src/db/queries/select.ts
import { asc, count, eq, getTableColumns, gt, 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(gt(postsTable.createdAt, sql`(datetime('now','-24 hour'))`))
    .orderBy(asc(postsTable.title), asc(postsTable.id))
    .limit(pageSize)
    .offset((page - 1) * pageSize);
}

λ˜λŠ” κ΄€κ³„ν˜• 쿼리 ꡬ문을 μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.

데이터 μ—…λ°μ΄νŠΈ

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

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

src/db/queries/delete.ts
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));
}