Drizzle 스키마

Drizzle을 사용하면 기본 데이터베이스에서 지원하는 다양한 모델과 속성을 TypeScript로 스키마를 정의할 수 있습니다. 스키마를 정의하면 향후 쿼리(Drizzle ORM 사용) 및 마이그레이션(Drizzle Kit 사용)에서 수정 사항의 기준이 됩니다.

마이그레이션 프로세스에 Drizzle Kit을 사용하는 경우, 스키마 파일에 정의된 모든 모델을 export해야 Drizzle Kit이 이를 import하여 마이그레이션 diff 프로세스에 사용할 수 있습니다.

Using imports
Using callback
Using import *
import { integer, pgTable, varchar } from "drizzle-orm/pg-core";

export const usersTable = pgTable("users", {
  id: integer().primaryKey().generatedAlwaysAsIdentity(),
  name: varchar().notNull(),
  age: integer().notNull(),
  email: varchar().notNull().unique(),
});

스키마 파일 구성

TypeScript에서 SQL 스키마를 단일 schema.ts 파일로 선언하거나 여러 파일로 분산할 수 있습니다. 원하는 방식을 자유롭게 선택하세요!

1개 파일에 스키마 정의

Drizzle로 스키마를 선언하는 가장 일반적인 방법은 모든 테이블을 하나의 schema.ts 파일에 배치하는 것입니다.

참고: 스키마 파일 이름은 원하는 대로 지정할 수 있습니다. 예를 들어 models.ts 또는 다른 이름을 사용할 수 있습니다.

이 방식은 정의된 테이블 모델이 많지 않거나 모든 테이블을 한 파일에 보관해도 괜찮은 경우에 적합합니다.

예제:

📦 <project root>
 └ 📂 src
    └ 📂 db
       └ 📜 schema.ts

drizzle.config.ts 파일에서 스키마 파일 경로를 지정해야 합니다. 이 설정으로 Drizzle은 schema.ts 파일을 읽고 마이그레이션 생성 프로세스에서 이 정보를 사용합니다. drizzle.config.ts 파일 및 Drizzle 마이그레이션에 대한 자세한 내용은 다음을 확인하세요: 링크

import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: 'postgresql', // 'mysql' | 'sqlite' | 'turso'
  schema: './src/db/schema.ts'
})

여러 파일에 스키마 정의

Drizzle 모델(테이블, enum, sequence 등)을 한 파일뿐만 아니라 원하는 모든 파일에 배치할 수 있습니다. 단, Drizzle Kit이 마이그레이션에서 import하여 사용할 수 있도록 해당 파일에서 모든 모델을 export해야 합니다.

사용 사례 중 하나는 각 테이블을 별도의 파일로 분리하는 것입니다.

📦 <project root>
 └ 📂 src
    └ 📂 db
       └ 📂 schema
          ├ 📜 users.ts
          ├ 📜 countries.ts
          ├ 📜 cities.ts
          ├ 📜 products.ts
          ├ 📜 clients.ts
          └ 📜 etc.ts

drizzle.config.ts 파일에서 스키마 폴더 경로를 지정해야 합니다. 이 설정으로 Drizzle은 schema 폴더를 읽고 재귀적으로 모든 파일을 찾아 모든 Drizzle 테이블을 가져옵니다. drizzle.config.ts 파일 및 Drizzle 마이그레이션에 대한 자세한 내용은 다음을 확인하세요: 링크

import { defineConfig } from "drizzle-kit";

export default defineConfig({
  dialect: 'postgresql', // 'mysql' | 'sqlite' | 'turso'
  schema: './src/db/schema'
})

사용자 관련 테이블, 메시징 관련 테이블, 제품 관련 테이블 등과 같이 원하는 방식으로 그룹화할 수도 있습니다.

📦 <project root>
 └ 📂 src
    └ 📂 db
       └ 📂 schema
          ├ 📜 users.ts
          ├ 📜 messaging.ts
          └ 📜 products.ts

데이터 스키마 구성

Drizzle 스키마는 사용 중인 데이터베이스의 여러 모델 타입으로 구성됩니다. Drizzle로 다음을 지정할 수 있습니다:

Drizzle로 스키마를 정의하는 방법을 하나씩 살펴보겠습니다.

테이블 및 컬럼 선언

Drizzle의 테이블은 데이터베이스에서와 마찬가지로 최소 1개의 컬럼으로 정의되어야 합니다. 한 가지 중요한 점은 Drizzle에는 공통 테이블 객체가 없다는 것입니다. 사용하는 dialect(PostgreSQL, MySQL, SQLite)를 선택해야 합니다.

PostgreSQL Table
MySQL Table
SQLite Table
import { pgTable, integer } from "drizzle-orm/pg-core"

export const users = pgTable('users', {
  id: integer()
});

기본적으로 Drizzle은 데이터베이스 쿼리에서 컬럼에 대해 TypeScript 키 이름을 사용합니다. 따라서 예제의 스키마와 쿼리는 아래와 같은 SQL 쿼리를 생성합니다.

이 예제는 db 객체를 사용하는데, 초기화 방법은 이 문서에서 다루지 않습니다. 데이터베이스 연결 방법을 알아보려면 연결 문서를 참조하세요.


TypeScript 키 = 데이터베이스 키

// schema.ts
import { integer, pgTable, varchar } from "drizzle-orm/pg-core";

export const users = pgTable('users', {
  id: integer(),
  first_name: varchar()
})
// query.ts
await db.select().from(users);
SELECT "id", "first_name" from users;

TypeScript 코드와 데이터베이스에서 서로 다른 이름을 사용하려면 컬럼 별칭을 사용할 수 있습니다.

// schema.ts
import { integer, pgTable, varchar } from "drizzle-orm/pg-core";

export const users = pgTable('users', {
  id: integer(),
  firstName: varchar('first_name')
})
// query.ts
await db.select().from(users);
SELECT "id", "first_name" from users;

Camel 및 Snake 케이싱

데이터베이스 모델 이름은 주로 snake_case 규칙을 사용하는 반면, TypeScript에서는 모델 이름에 camelCase를 사용하는 것이 일반적입니다. 이로 인해 스키마에 많은 별칭을 정의해야 할 수 있습니다. 이 문제를 해결하기 위해 Drizzle은 데이터베이스 초기화 시 선택적 매개변수를 포함하여 TypeScript의 camelCase를 데이터베이스의 snake_case로 자동 매핑하는 방법을 제공합니다.

이러한 매핑을 위해 Drizzle DB 선언에서 casing 옵션을 사용할 수 있습니다. 이 매개변수는 데이터베이스 모델 명명 규칙을 지정하고 모든 JavaScript 키를 그에 따라 매핑하려고 시도합니다.

// schema.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { integer, pgTable, varchar } from "drizzle-orm/pg-core";

export const users = pgTable('users', {
  id: integer(),
  firstName: varchar()
})
// db.ts
const db = drizzle({ connection: process.env.DATABASE_URL, casing: 'snake_case' })
// query.ts
await db.select().from(users);
SELECT "id", "first_name" from users;

고급 기능

Drizzle ORM에서 사용할 수 있는 몇 가지 기법이 있습니다. Drizzle이 완전히 TypeScript 파일에 있는 한, 일반 TypeScript 프로젝트에서 하는 것과 동일한 작업을 본질적으로 수행할 수 있습니다.

일반적인 기능 중 하나는 컬럼을 다른 위치로 분리한 다음 재사용하는 것입니다. 예를 들어 updated_at, created_at, deleted_at 컬럼을 생각해 보세요. 많은 테이블/모델이 시스템의 엔티티 생성, 삭제, 업데이트를 추적하고 분석하기 위해 이 세 가지 필드가 필요할 수 있습니다.

이러한 컬럼을 별도의 파일에 정의한 다음 import하여 모든 테이블 객체에 spread할 수 있습니다.

// columns.helpers.ts
const timestamps = {
  updated_at: timestamp(),
  created_at: timestamp().defaultNow().notNull(),
  deleted_at: timestamp(),
}
// users.sql.ts
export const users = pgTable('users', {
  id: integer(),
  ...timestamps
})
// posts.sql.ts
export const posts = pgTable('posts', {
  id: integer(),
  ...timestamps
})

스키마

PostgreSQL
MySQL
SQLite


PostgreSQL에는 schema라는 엔티티가 있습니다(우리는 이것을 folders라고 불러야 한다고 생각합니다). 이는 PostgreSQL에서 다음과 같은 구조를 생성합니다:

pgSchema로 PostgreSQL 스키마를 관리하고 그 안에 다른 모델을 배치할 수 있습니다.

Drizzle로 관리하려는 스키마를 정의합니다.

import { pgSchema } from "drizzle-orm/pg-core"

export const customSchema = pgSchema('custom');

그런 다음 스키마 객체 내부에 테이블을 배치합니다.

import { integer, pgSchema } from "drizzle-orm/pg-core";

export const customSchema = pgSchema('custom');

export const users = customSchema.table('users', {
  id: integer()
})

예제

기본 사항을 알았으니 실제 프로젝트를 위한 스키마 예제를 정의하여 더 나은 이해를 얻어봅시다.

모든 예제는 generateUniqueString을 사용합니다. 구현 코드는 모든 스키마 예제 다음에 제공됩니다.

PostgreSQL
MySQL
SQLite
import { AnyPgColumn } from "drizzle-orm/pg-core";
import { pgEnum, pgTable as table } from "drizzle-orm/pg-core";
import * as t from "drizzle-orm/pg-core";

export const rolesEnum = pgEnum("roles", ["guest", "user", "admin"]);

export const users = table(
  "users",
  {
    id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
    firstName: t.varchar("first_name", { length: 256 }),
    lastName: t.varchar("last_name", { length: 256 }),
    email: t.varchar().notNull(),
    invitee: t.integer().references((): AnyPgColumn => users.id),
    role: rolesEnum().default("guest"),
  },
  (table) => [
    t.uniqueIndex("email_idx").on(table.email)
  ]
);

export const posts = table(
  "posts",
  {
    id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
    slug: t.varchar().$default(() => generateUniqueString(16)),
    title: t.varchar({ length: 256 }),
    ownerId: t.integer("owner_id").references(() => users.id),
  },
  (table) => [
    t.uniqueIndex("slug_idx").on(table.slug),
    t.index("title_idx").on(table.title),
  ]
);

export const comments = table("comments", {
  id: t.integer().primaryKey().generatedAlwaysAsIdentity(),
  text: t.varchar({ length: 256 }),
  postId: t.integer("post_id").references(() => posts.id),
  ownerId: t.integer("owner_id").references(() => users.id),
});

generateUniqueString 구현:

function generateUniqueString(length: number = 12): string {
  const characters =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  let uniqueString = "";

  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * characters.length);
    uniqueString += characters[randomIndex];
  }

  return uniqueString;
}

다음 단계