Drizzle Seed

PostgreSQL
SQLite
MySQL
SingleStore
IMPORTANT

drizzle-seeddrizzle-orm@0.36.4 이상 버전에서만 사용할 수 있습니다. 이보다 낮은 버전은 런타임에서 동작할 수 있지만, 타입 문제와 identity column 문제가 발생할 수 있습니다. 이 패치는 drizzle-orm@0.36.4에서 도입되었습니다.

drizzle-seed는 데이터베이스에 채울 수 있는 결정적이면서도 현실적인 가짜 데이터를 생성하는 데 도움을 주는 TypeScript 라이브러리입니다. 시드 가능한 의사 난수 생성기(pRNG)를 활용하여, 생성된 데이터가 여러 실행에서 일관되고 재현 가능하도록 보장합니다. 이는 테스트, 개발, 디버깅 목적으로 특히 유용합니다.

결정적 데이터 생성이란?

결정적 데이터 생성은 동일한 입력이 항상 동일한 출력을 생성한다는 것을 의미합니다.
drizzle-seed 라이브러리의 경우, 동일한 시드(seed) 번호로 초기화하면 매번 같은 순서의 가짜 데이터를 생성합니다. 이를 통해 예측 가능하고 반복 가능한 데이터 세트를 만들 수 있습니다.

의사 난수 생성기 (pRNG)

의사 난수 생성기(pseudorandom number generator, pRNG)는 난수의 특성을 근사적으로 모방한 숫자 시퀀스를 생성하는 알고리즘입니다. 하지만 이 알고리즘은 시드(seed)라고 불리는 초기값에 기반하기 때문에, 난수의 생성 과정을 제어할 수 있습니다. 동일한 시드를 사용하면 pRNG는 항상 같은 숫자 시퀀스를 생성하므로, 데이터 생성 과정을 재현 가능하게 만들 수 있습니다.

pRNG 사용의 장점:

drizzle-seed를 사용하면 두 가지 장점을 모두 누릴 수 있습니다. 현실적인 가짜 데이터를 생성할 수 있을 뿐만 아니라, 필요할 때마다 이를 재현할 수 있는 제어 기능도 제공합니다.

설치

npm
yarn
pnpm
bun
npm i drizzle-seed

drizzle-seed 패키지를 설치하려면 위의 명령어를 사용하세요. 이 명령어는 npm을 통해 패키지를 설치합니다.

기본 사용법

이 예제에서는 무작위 이름과 ID를 가진 10명의 사용자를 생성합니다.

import { pgTable, integer, text } from "drizzle-orm/pg-core";
import { drizzle } from "drizzle-orm/node-postgres";
import { seed } from "drizzle-seed";

const users = pgTable("users", {
  id: integer().primaryKey(),
  name: text().notNull(),
});

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);
  await seed(db, { users });
}

main();

코드 설명

  1. pgTable: PostgreSQL 테이블을 정의합니다. users 테이블에는 idname 두 개의 컬럼이 있습니다.

    • id: 정수 타입의 기본 키(primary key)입니다.
    • name: 텍스트 타입이며, null 값을 허용하지 않습니다.
  2. drizzle: 데이터베이스 연결을 설정합니다. 환경 변수 DATABASE_URL을 사용해 데이터베이스에 접속합니다.

  3. seed: 데이터베이스에 초기 데이터를 삽입합니다. users 테이블에 무작위 데이터를 생성합니다.

  4. main 함수: 데이터베이스 연결과 초기 데이터 삽입을 실행합니다.

옵션

count

기본적으로 seed 함수는 10개의 엔티티를 생성합니다. 하지만 테스트를 위해 더 많은 엔티티가 필요하다면, 시드 옵션 객체에서 이 값을 지정할 수 있습니다.

await seed(db, schema, { count: 1000 });

seed

모든 후속 실행에서 다른 값 세트를 생성하기 위해 시드가 필요하다면, seed 옵션에 다른 숫자를 정의할 수 있습니다. 새로운 숫자는 고유한 값 세트를 생성합니다.

await seed(db, schema, { seed: 12345 });

데이터베이스 초기화

drizzle-seed를 사용하면 데이터베이스를 쉽게 초기화하고 새로운 값으로 시드할 수 있습니다. 예를 들어, 테스트 스위트에서 유용하게 사용할 수 있습니다.

// 초기화할 스키마 파일 경로
import * as schema from "./schema.ts";
import { reset } from "drizzle-seed";

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);
  await reset(db, schema);
}

main();

데이터베이스 초기화는 각각의 데이터베이스 방언(dialect)에 따라 다른 전략을 사용합니다.

PostgreSQL
MySQL
SQLite

PostgreSQL의 경우, drizzle-seed 패키지는 CASCADE 옵션을 포함한 TRUNCATE 문을 생성하여 초기화 함수 실행 후 모든 테이블이 비어 있도록 보장합니다.

TRUNCATE tableName1, tableName2, ... CASCADE;

세부 조정

drizzle-seed가 기본적으로 사용하는 시드 생성 함수의 동작을 변경해야 한다면, 여러분만의 구현을 지정할 수 있고, 심지어 시딩 과정에서 사용할 값 목록도 직접 정의할 수 있습니다.

.refinedrizzle-seed에서 제공하는 모든 생성 함수 목록을 받는 콜백입니다. 이 콜백은 조정하려는 테이블을 키로 가지는 객체를 반환해야 하며, 각 테이블의 동작을 필요에 따라 정의할 수 있습니다. 각 테이블은 데이터베이스 시딩을 단순화하기 위해 여러 속성을 지정할 수 있습니다:

info

참조 값의 수를 생성할 때 가중치를 적용한 랜덤 분포를 지정할 수도 있습니다. 이 API에 대한 자세한 내용은 가중치 랜덤 문서를 참조하세요.

API

await seed(db, schema).refine((f) => ({
  users: {
    columns: {},
    count: 10,
    with: {
        posts: 10
    }
  },
}));

다음은 몇 가지 예제와 함께 어떤 일이 발생하는지 설명합니다:

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

export const users = pgTable("users", {
  id: integer().primaryKey(),
  name: text().notNull(),
});

export const posts = pgTable("posts", {
  id: integer().primaryKey(),
  description: text(),
  userId: integer().references(() => users.id),
});

예제 1: users 테이블에 20개의 엔티티를 시드하고, name 컬럼에 대한 시드 로직을 조정합니다.

index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, { users: schema.users }).refine((f) => ({
    users: {
        columns: {
            name: f.fullName(),
        },
        count: 20
    }
  }));
}

main();

예제 2: users 테이블에 20개의 엔티티를 시드하고, 각 user에 대해 10개의 posts를 추가합니다. 이때 posts 테이블을 시드하고 posts에서 users로의 참조를 생성합니다.

index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, schema).refine((f) => ({
    users: {
        count: 20,
        with: {
            posts: 10
        }
    }
  }));
}

main();

예제 3: users 테이블에 5개의 엔티티를 시드하고, 데이터베이스에 100개의 posts를 추가하되 users 엔티티와 연결하지 않습니다. usersid 생성을 조정하여 10000에서 20000 사이의 고유한 정수를 생성하도록 하고, posts의 값을 직접 정의한 배열에서 가져오도록 조정합니다.

index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, schema).refine((f) => ({
    users: {
        count: 5,
        columns: {
            id: f.int({
              minValue: 10000,
              maxValue: 20000,
              isUnique: true,
            }),
        }
    },
    posts: {
        count: 100,
        columns: {
            description: f.valuesFromArray({
            values: [
                "The sun set behind the mountains, painting the sky in hues of orange and purple", 
                "I can't believe how good this homemade pizza turned out!", 
                "Sometimes, all you need is a good book and a quiet corner.", 
                "Who else thinks rainy days are perfect for binge-watching old movies?", 
                "Tried a new hiking trail today and found the most amazing waterfall!",
                // ...
            ],
          })
        }
    }
  }));
}

main();
IMPORTANT

이 문서에서 더 많은 가능성을 정의할 예정이지만, 지금은 이 문서의 몇 가지 섹션을 탐색할 수 있습니다. 사용 가능한 모든 생성 함수에 익숙해지려면 생성자 섹션을 확인하세요.

특히 유용한 기능은 컬럼에 대해 생성된 값과 drizzle-seed가 생성할 수 있는 관련 엔티티의 수를 결정할 때 가중치를 적용한 랜덤화를 사용할 수 있다는 점입니다.

자세한 내용은 가중치 랜덤 문서를 확인하세요.

가중치 랜덤(Weighted Random)

시드(seed) 단계에서 데이터베이스에 삽입할 여러 데이터셋에 서로 다른 우선순위를 적용해야 하는 경우가 있습니다. 이런 경우를 위해 Drizzle Seed는 **가중치 랜덤(weighted random)**이라는 API를 제공합니다.

Drizzle Seed 패키지에서 가중치 랜덤을 사용할 수 있는 몇 가지 경우는 다음과 같습니다:

두 가지 예제를 살펴보겠습니다.

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

export const orders = pgTable(
  "orders",
  {
    id: integer().primaryKey(),
    name: text().notNull(),
    quantityPerUnit: varchar().notNull(),
    unitPrice: doublePrecision().notNull(),
    unitsInStock: integer().notNull(),
    unitsOnOrder: integer().notNull(),
    reorderLevel: integer().notNull(),
    discontinued: integer().notNull(),
  }
);

export const details = pgTable(
  "details",
  {
    unitPrice: doublePrecision().notNull(),
    quantity: integer().notNull(),
    discount: doublePrecision().notNull(),

    orderId: integer()
      .notNull()
      .references(() => orders.id, { onDelete: "cascade" }),
  }
);

예제 1: unitPrice 생성 로직을 세부 설정하여 5000개의 랜덤 가격을 생성합니다. 이때, 10-100 사이의 가격이 30% 확률로, 100-300 사이의 가격이 70% 확률로 생성되도록 설정합니다.

index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, schema).refine((f) => ({
    orders: {
       count: 5000,
       columns: {
           unitPrice: f.weightedRandom(
               [
                   {
                       weight: 0.3,
                       value: funcs.int({ minValue: 10, maxValue: 100 })
                   },
                   {
                       weight: 0.7,
                       value: funcs.number({ minValue: 100, maxValue: 300, precision: 100 })
                   }
               ]
           ),
       }
    }
  }));
}

main();

예제 2: 각 주문에 대해 13개의 상세 정보를 60% 확률로, 57개의 상세 정보를 30% 확률로, 8~10개의 상세 정보를 10% 확률로 생성합니다.

index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { seed } from "drizzle-seed";
import * as schema from './schema.ts'

async function main() {
  const db = drizzle(process.env.DATABASE_URL!);

  await seed(db, schema).refine((f) => ({
    orders: {
       with: {
           details:
               [
                   { weight: 0.6, count: [1, 2, 3] },
                   { weight: 0.3, count: [5, 6, 7] },
                   { weight: 0.1, count: [8, 9, 10] },
               ]
       }
    }
  }));
}

main();

복잡한 예제

main.ts
schema.ts
import { seed } from "drizzle-seed";
import * as schema from "./schema.ts";

const main = async () => {
    const titlesOfCourtesy = ["Ms.", "Mrs.", "Dr."];
    const unitsOnOrders = [0, 10, 20, 30, 50, 60, 70, 80, 100];
    const reorderLevels = [0, 5, 10, 15, 20, 25, 30];
    const quantityPerUnit = [
        "100 - 100 g pieces",
        "100 - 250 g bags",
        "10 - 200 g glasses",
        "10 - 4 oz boxes",
        "10 - 500 g pkgs.",
        "10 - 500 g pkgs."
    ];
    const discounts = [0.05, 0.15, 0.2, 0.25];

    await seed(db, schema).refine((funcs) => ({
        customers: {
            count: 10000,
            columns: {
                companyName: funcs.companyName(),
                contactName: funcs.fullName(),
                contactTitle: funcs.jobTitle(),
                address: funcs.streetAddress(),
                city: funcs.city(),
                postalCode: funcs.postcode(),
                region: funcs.state(),
                country: funcs.country(),
                phone: funcs.phoneNumber({ template: "(###) ###-####" }),
                fax: funcs.phoneNumber({ template: "(###) ###-####" })
            }
        },
        employees: {
            count: 200,
            columns: {
                firstName: funcs.firstName(),
                lastName: funcs.lastName(),
                title: funcs.jobTitle(),
                titleOfCourtesy: funcs.valuesFromArray({ values: titlesOfCourtesy }),
                birthDate: funcs.date({ minDate: "2010-12-31", maxDate: "2010-12-31" }),
                hireDate: funcs.date({ minDate: "2010-12-31", maxDate: "2024-08-26" }),
                address: funcs.streetAddress(),
                city: funcs.city(),
                postalCode: funcs.postcode(),
                country: funcs.country(),
                homePhone: funcs.phoneNumber({ template: "(###) ###-####" }),
                extension: funcs.int({ minValue: 428, maxValue: 5467 }),
                notes: funcs.loremIpsum()
            }
        },
        orders: {
            count: 50000,
            columns: {
                shipVia: funcs.int({ minValue: 1, maxValue: 3 }),
                freight: funcs.number({ minValue: 0, maxValue: 1000, precision: 100 }),
                shipName: funcs.streetAddress(),
                shipCity: funcs.city(),
                shipRegion: funcs.state(),
                shipPostalCode: funcs.postcode(),
                shipCountry: funcs.country()
            },
            with: {
                details:
                    [
                        { weight: 0.6, count: [1, 2, 3, 4] },
                        { weight: 0.2, count: [5, 6, 7, 8, 9, 10] },
                        { weight: 0.15, count: [11, 12, 13, 14, 15, 16, 17] },
                        { weight: 0.05, count: [18, 19, 20, 21, 22, 23, 24, 25] },
                    ]
            }
        },
        suppliers: {
            count: 1000,
            columns: {
                companyName: funcs.companyName(),
                contactName: funcs.fullName(),
                contactTitle: funcs.jobTitle(),
                address: funcs.streetAddress(),
                city: funcs.city(),
                postalCode: funcs.postcode(),
                region: funcs.state(),
                country: funcs.country(),
                phone: funcs.phoneNumber({ template: "(###) ###-####" })
            }
        },
        products: {
            count: 5000,
            columns: {
                name: funcs.companyName(),
                quantityPerUnit: funcs.valuesFromArray({ values: quantityPerUnit }),
                unitPrice: funcs.weightedRandom(
                    [
                        {
                            weight: 0.5,
                            value: funcs.int({ minValue: 3, maxValue: 300 })
                        },
                        {
                            weight: 0.5,
                            value: funcs.number({ minValue: 3, maxValue: 300, precision: 100 })
                        }
                    ]
                ),
                unitsInStock: funcs.int({ minValue: 0, maxValue: 125 }),
                unitsOnOrder: funcs.valuesFromArray({ values: unitsOnOrders }),
                reorderLevel: funcs.valuesFromArray({ values: reorderLevels }),
                discontinued: funcs.int({ minValue: 0, maxValue: 1 })
            }
        },
        details: {
            columns: {
                unitPrice: funcs.number({ minValue: 10, maxValue: 130 }),
                quantity: funcs.int({ minValue: 1, maxValue: 130 }),
                discount: funcs.weightedRandom(
                    [
                        { weight: 0.5, value: funcs.valuesFromArray({ values: discounts }) },
                        { weight: 0.5, value: funcs.default({ defaultValue: 0 }) }
                    ]
                )
            }
        }
    }));
}

main();

Limitations

with 옵션의 타입 제한 사항

Drizzle의 현재 API와 TypeScript의 몇 가지 제한 사항 때문에, 특히 테이블 간 순환 참조가 존재할 때 테이블 간의 참조를 올바르게 추론하는 것이 불가능합니다.

이로 인해 with 옵션은 스키마 내의 모든 테이블을 표시하며, 여러분이 일대다 관계를 가진 테이블을 직접 선택해야 합니다.

경고

with 옵션은 일대다 관계에서만 작동합니다. 예를 들어, 하나의 user와 여러 개의 posts가 있는 경우, user와 함께 posts를 사용할 수 있지만, posts와 함께 user를 사용할 수는 없습니다.

Drizzle 테이블의 세 번째 매개변수 타입 제한 사항:

현재 Drizzle 테이블에서 세 번째 매개변수에 대한 타입 지원이 없습니다. 런타임에서는 동작하지만, 타입 레벨에서는 정상적으로 작동하지 않습니다.