import { drizzle } from 'drizzle-orm/node-postgres';import dotenv from "dotenv/config";import { sql } from "drizzle-orm";import { AsyncLocalStorage } from "async_hooks";export constdb = drizzle(process.env.NILEDB_URL);export const tenantContext = new AsyncLocalStorage<string | undefined>();export function tenantDB<T>(cb: (tx: any) => T | Promise<T>): Promise<T> { returndb.transaction(async (tx) => { const tenantId = tenantContext.getStore(); console.log("executing query with tenant: " + tenantId); // if there's a tenant ID, set it in the transaction context if (tenantId) { await tx.execute(sql`set local nile.tenant_id = '${sql.raw(tenantId)}'`); } return cb(tx); }) as Promise<T>;}
Drizzle 설정 파일 설정하기
Drizzle config - Drizzle Kit에서 사용하는 설정 파일로, 데이터베이스 연결, 마이그레이션 폴더, 스키마 파일에 대한 모든 정보를 포함합니다.
Nile 데이터베이스에는 내장 테이블이 있습니다. 가장 중요한 테이블은 테넌트를 생성하고 관리하는 데 사용되는 tenants 테이블입니다.
애플리케이션에서 이 테이블을 사용하기 위해 Drizzle Kit CLI를 사용하여 이 스키마를 포함하는 스키마 파일을 생성하겠습니다.
npx drizzle-kit pull
인트로스펙션 결과로 schema.ts 파일, 데이터베이스 스키마 스냅샷이 포함된 meta 폴더, 마이그레이션 sql 파일, 관계형 쿼리를 위한 relations.ts 파일이 생성됩니다.
We recommend transferring the generated code from drizzle/schema.ts and drizzle/relations.ts to the actual schema file. In this guide we transferred code to src/db/schema.ts. Generated files for schema and relations can be deleted. This way you can manage your schema in a more structured way.
You can directly apply changes to your database using the drizzle-kit push command. This is a convenient method for quickly testing new schema designs or modifications in a local development environment, allowing for rapid iterations without the need to manage migration files:
이제 Drizzle을 Nile에 연결하도록 설정하고 스키마를 준비했으므로, 멀티 테넌트 웹 애플리케이션에서 사용할 수 있습니다.
이 예제에서는 Express를 웹 프레임워크로 사용하지만, Nile과 Drizzle은 모든 웹 프레임워크에서 사용할 수 있습니다.
예제를 간단하게 유지하기 위해 단일 파일 src/app.ts에 웹앱을 구현하겠습니다. 먼저 웹앱을 초기화합니다:
src/app.ts
import express from "express";import { tenantDB, tenantContext, db } from "./db/db";import { tenants as tenantSchema, todos as todoSchema,} from "./db/schema";import { eq } from "drizzle-orm";const PORT = process.env.PORT || 3001;const app = express();app.listen(PORT, () => console.log(`Server is running on port ${PORT}`));app.use(express.json());
테넌트 인식 미들웨어 초기화하기
다음으로 예제에 미들웨어를 추가하겠습니다. 이 미들웨어는 경로 매개변수에서 테넌트 ID를 가져와 AsyncLocalStorage에 저장합니다.
src/db/index.ts에서 생성한 tenantDB 래퍼는 쿼리 실행 시 이 테넌트 ID를 사용하여 nile.tenant_id를 설정하며,
이를 통해 쿼리가 해당 테넌트의 가상 데이터베이스에 대해 실행되는 것을 보장합니다.
src/app.ts
// set the tenant ID in the context based on the URL parameterapp.use('/api/tenants/:tenantId/*', (req, res, next) => { const tenantId = req.params.tenantId; console.log("setting context to tenant: " + tenantId); tenantContext.run(tenantId, next);});
이 예제는 경로 매개변수에서 테넌트 ID를 가져오지만, x-tenant-id와 같은 헤더나 쿠키에 테넌트 ID를 설정하는 것도 일반적입니다.
라우트 추가하기
마지막으로 테넌트와 할 일을 생성하고 조회하는 라우트를 추가해야 합니다. tenantDB 래퍼를 사용하여 테넌트의 가상 데이터베이스에 연결하는 방법을 주목하세요.
또한 app.get("/api/tenants/:tenantId/todos"에서 쿼리에 where tenant_id=...를 지정할 필요가 없다는 점도 주목하세요.
이는 정확히 해당 테넌트의 데이터베이스로 라우팅되기 때문이며, 쿼리는 다른 테넌트의 데이터를 반환할 수 없습니다.
src/app.ts
// create new tenantapp.post("/api/tenants", async (req, res) => { try { const name = req.body.name; var tenants: any = null; tenants = await tenantDB(async (tx) => { return await tx.insert(tenantSchema).values({ name }).returning(); }); res.json(tenants); } catch (error: any) { console.log("error creating tenant: " + error.message); res.status(500).json({message: "Internal Server Error",}); }});// return list of tenantsapp.get("/api/tenants", async (req, res) => { let tenants: any = []; try { tenants = await tenantDB(async (tx) => { return await tx.select().from(tenantSchema); }); res.json(tenants); } catch (error: any) { console.log("error listing tenants: " + error.message); res.status(500).json({message: "Internal Server Error",}); }});// add new task for tenantapp.post("/api/tenants/:tenantId/todos", async (req, res) => { try { const { title, complete } = req.body; if (!title) { res.status(400).json({message: "No task title provided",}); } const tenantId = req.params.tenantId; const newTodo = await tenantDB(async (tx) => { return await tx .insert(todoSchema) .values({ tenantId, title, complete }) .returning(); }); // return without the embedding vector, since it is huge and useless res.json(newTodo); } catch (error: any) { console.log("error adding task: " + error.message); res.status(500).json({message: "Internal Server Error",}); }});// update tasks for tenant// No need for where clause because we have the tenant in the contextapp.put("/api/tenants/:tenantId/todos", async (req, res) => { try { const { id, complete } = req.body; await tenantDB(async (tx) => { return await tx .update(todoSchema) .set({ complete }) .where(eq(todoSchema.id, id)); }); res.sendStatus(200); } catch (error: any) { console.log("error updating tasks: " + error.message); res.status(500).json({message: "Internal Server Error",}); }});// get all tasks for tenantapp.get("/api/tenants/:tenantId/todos", async (req, res) => { try { // No need for a "where" clause here because we are setting the tenant ID in the context const todos = await tenantDB(async (tx) => { return await tx .select({ id: todoSchema.id, tenant_id: todoSchema.tenantId, title: todoSchema.title, estimate: todoSchema.estimate, }) .from(todoSchema); }); res.json(todos); } catch (error: any) { console.log("error listing tasks: " + error.message); res.status(500).json({message: error.message,}); }});
실행해보기!
이제 새로운 웹 애플리케이션을 실행할 수 있습니다:
npx tsx src/app.ts
그리고 curl을 사용하여 방금 생성한 라우트를 테스트할 수 있습니다:
# create a tenantcurl --location --request POST 'localhost:3001/api/tenants' \--header 'Content-Type: application/json' \--data-raw '{"name":"my first customer"}'# get tenantscurl -X GET 'http://localhost:3001/api/tenants'# create a todo (don't forget to use a real tenant-id in the URL)curl -X POST \ 'http://localhost:3001/api/tenants/108124a5-2e34-418a-9735-b93082e9fbf2/todos' \ --header 'Content-Type: application/json' \ --data-raw '{"title": "feed the cat", "complete": false}'# list todos for tenant (don't forget to use a real tenant-id in the URL)curl -X GET \ 'http://localhost:3001/api/tenants/108124a5-2e34-418a-9735-b93082e9fbf2/todos'
프로젝트 파일 구조
다음은 프로젝트의 파일 구조입니다. src/db 디렉토리에는 db.ts의 연결 설정과 schema.ts의 스키마 정의를 포함한 데이터베이스 관련 파일이 있습니다.
마이그레이션 및 인트로스펙션으로 생성된 파일은 ./drizzle에 있습니다