Drizzle <> Nile

This guide assumes familiarity with:

**๊ณต์‹ ์›น์‚ฌ์ดํŠธ**์— ๋”ฐ๋ฅด๋ฉด, Nile์€ ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์•ฑ์„ ์œ„ํ•ด ์žฌ์„ค๊ณ„๋œ PostgreSQL์ž…๋‹ˆ๋‹ค.

๊ณต์‹ Nile + Drizzle ๋น ๋ฅธ ์‹œ์ž‘ ๋ฐ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ฌธ์„œ๋ฅผ ํ™•์ธํ•˜์„ธ์š”.

Nile์€ Drizzle์˜ ๋ชจ๋“  Postgres ๋“œ๋ผ์ด๋ฒ„์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์•„๋ž˜์—์„œ๋Š” node-postgres ์‚ฌ์šฉ ์˜ˆ์ œ๋ฅผ ๋ณด์—ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

1๋‹จ๊ณ„ - ํŒจํ‚ค์ง€ ์„ค์น˜

npm
yarn
pnpm
bun
npm i drizzle-orm postgres
npm i -D drizzle-kit

2๋‹จ๊ณ„ - ๋“œ๋ผ์ด๋ฒ„ ์ดˆ๊ธฐํ™” ๋ฐ ์ฟผ๋ฆฌ ์‹คํ–‰

index.ts
// 'pg' ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
import { drizzle } from 'drizzle-orm/node-postgres'

const db = drizzle(process.env.NILEDB_URL);

const response = await db.select().from(...);

๊ธฐ์กด ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์ œ๊ณตํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ:

index.ts
// 'pg' ํŒจํ‚ค์ง€๋ฅผ ์„ค์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});
const db = drizzle({ client: pool });

const response = await db.select().from(...);

๊ฐ€์ƒ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—ฐ๊ฒฐ

Nile์€ ๊ฐ€์ƒ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ๋ฅผ ์„ค์ •ํ•˜๋ฉด Nile์€ ํ•ด๋‹น ํŠน์ • ํ…Œ๋„ŒํŠธ์˜ ๊ฐ€์ƒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋กœ ์ฟผ๋ฆฌ๋ฅผ ์ „๋‹ฌํ•˜๊ณ  ๋ชจ๋“  ์ฟผ๋ฆฌ๊ฐ€ ํ•ด๋‹น ํ…Œ๋„ŒํŠธ์— ์ ์šฉ๋ฉ๋‹ˆ๋‹ค (์ฆ‰, select * from table์€ ์ด ํ…Œ๋„ŒํŠธ์˜ ๋ ˆ์ฝ”๋“œ๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค).

ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ๋ฅผ ์„ค์ •ํ•˜๊ธฐ ์œ„ํ•ด, ํŠธ๋žœ์žญ์…˜์„ ์‹คํ–‰ํ•˜๊ธฐ ์ „์— ์ ์ ˆํ•œ ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ๋ฅผ ์„ค์ •ํ•˜๋Š” ํŠธ๋žœ์žญ์…˜์œผ๋กœ ๊ฐ ์ฟผ๋ฆฌ๋ฅผ ๋ž˜ํ•‘ํ•ฉ๋‹ˆ๋‹ค.

ํ…Œ๋„ŒํŠธ ID๋Š” ๊ฐ„๋‹จํžˆ ๋ž˜ํผ์— ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

index.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { todosTable, tenants } from "./db/schema";
import { sql } from 'drizzle-orm';
import 'dotenv/config';

const db = drizzle(process.env.NILEDB_URL);

function tenantDB<T>(tenantId: string, cb: (tx: any) => T | Promise<T>): Promise<T> {
  return db.transaction(async (tx) => {
    if (tenantId) {
      await tx.execute(sql`set local nile.tenant_id = '${sql.raw(tenantId)}'`);
    }

    return cb(tx);
  }) as Promise<T>;
}

// ์›น ์•ฑ์—์„œ๋Š” ์š”์ฒญ ๊ฒฝ๋กœ ๋งค๊ฐœ๋ณ€์ˆ˜๋‚˜ ํ—ค๋”์—์„œ ๊ฐ€์ ธ์˜ฌ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค
const tenantId = '01943e56-16df-754f-a7b6-6234c368b400'

const response = await tenantDB(tenantId, async (tx) => {
    // ์—ฌ๊ธฐ์— "where" ์ ˆ์ด ํ•„์š”ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค
    return await tx.select().from(todosTable);
});

console.log(response);

์ด๋ฅผ ์ง€์›ํ•˜๋Š” ์›น ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, AsyncLocalStorage๋ฅผ ์„ค์ •ํ•˜๊ณ  ๋ฏธ๋“ค์›จ์–ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ…Œ๋„ŒํŠธ ID๋กœ ์ฑ„์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๊ฒฝ์šฐ Drizzle ํด๋ผ์ด์–ธํŠธ ์„ค์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

import { drizzle } from 'drizzle-orm/node-postgres';
import dotenv from "dotenv/config";
import { sql } from "drizzle-orm";
import { AsyncLocalStorage } from "async_hooks";

export const db = drizzle(process.env.NILEDB_URL);
export const tenantContext = new AsyncLocalStorage<string | undefined>();

export function tenantDB<T>(cb: (tx: any) => T | Promise<T>): Promise<T> {
  return db.transaction(async (tx) => {
    const tenantId = tenantContext.getStore();
    console.log("executing query with tenant: " + tenantId);
    // ํ…Œ๋„ŒํŠธ ID๊ฐ€ ์žˆ์œผ๋ฉด ํŠธ๋žœ์žญ์…˜ ์ปจํ…์ŠคํŠธ์— ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค
    if (tenantId) {
      await tx.execute(sql`set local nile.tenant_id = '${sql.raw(tenantId)}'`);
    }

    return cb(tx);
  }) as Promise<T>;
}

๊ทธ๋Ÿฐ ๋‹ค์Œ, AsyncLocalStorage๋ฅผ ์ฑ„์šฐ๊ณ  ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ๋•Œ tenantDB ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋„๋ก ๋ฏธ๋“ค์›จ์–ด๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค:

app.ts
// ํ…Œ๋„ŒํŠธ ์ปจํ…์ŠคํŠธ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฏธ๋“ค์›จ์–ด
app.use("/api/tenants/:tenantId/*", async (c, next) => {
  const tenantId = c.req.param("tenantId");
  console.log("setting context to tenant: " + tenantId);
  return tenantContext.run(tenantId, () => next());
});

// ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ
app.get("/api/tenants/:tenantId/todos", async (c) => {
    const todos = await tenantDB(c, async (tx) => {
      return await tx
        .select({
          id: todoSchema.id,
          tenant_id: todoSchema.tenantId,
          title: todoSchema.title,
          estimate: todoSchema.estimate,
        })
        .from(todoSchema);
    });
    return c.json(todos);
});

๋‹ค์Œ ๋‹จ๊ณ„