Drizzle Seed
IMPORTANT
drizzle-seed
는 drizzle-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 사용의 장점:
일관성 : 테스트를 실행할 때마다 동일한 데이터를 사용할 수 있습니다.
디버깅 : 일관된 데이터 세트를 제공해 버그를 재현하고 수정하기가 더 쉬워집니다.
협업 : 팀원들이 시드(seed) 번호를 공유해 동일한 데이터 세트로 작업할 수 있습니다.
drizzle-seed를 사용하면 두 가지 장점을 모두 누릴 수 있습니다. 현실적인 가짜 데이터를 생성할 수 있을 뿐만 아니라, 필요할 때마다 이를 재현할 수 있는 제어 기능도 제공합니다.
설치
npm i drizzle-seed
yarn add drizzle-seed
pnpm add drizzle-seed
bun add 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 ();
코드 설명
pgTable
: PostgreSQL 테이블을 정의합니다. users
테이블에는 id
와 name
두 개의 컬럼이 있습니다.
id
: 정수 타입의 기본 키(primary key)입니다.
name
: 텍스트 타입이며, null
값을 허용하지 않습니다.
drizzle
: 데이터베이스 연결을 설정합니다. 환경 변수 DATABASE_URL
을 사용해 데이터베이스에 접속합니다.
seed
: 데이터베이스에 초기 데이터를 삽입합니다. users
테이블에 무작위 데이터를 생성합니다.
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의 경우, drizzle-seed
패키지는 CASCADE
옵션을 포함한 TRUNCATE
문을 생성하여 초기화 함수 실행 후 모든 테이블이 비어 있도록 보장합니다.
TRUNCATE tableName1, tableName2, ... CASCADE;
MySQL의 경우, drizzle-seed
패키지는 먼저 FOREIGN_KEY_CHECKS
를 비활성화하여 다음 단계가 실패하지 않도록 한 후, 모든 테이블의 내용을 비우기 위해 TRUNCATE
문을 생성합니다.
SET FOREIGN_KEY_CHECKS = 0 ;
TRUNCATE tableName1;
TRUNCATE tableName2;
...
SET FOREIGN_KEY_CHECKS = 1 ;
SQLite의 경우, drizzle-seed
패키지는 먼저 foreign_keys
프라그마를 비활성화하여 다음 단계가 실패하지 않도록 한 후, 모든 테이블의 내용을 비우기 위해 DELETE FROM
문을 생성합니다.
PRAGMA foreign_keys = OFF ;
DELETE FROM tableName1;
DELETE FROM tableName2;
...
PRAGMA foreign_keys = ON ;
세부 조정
drizzle-seed
가 기본적으로 사용하는 시드 생성 함수의 동작을 변경해야 한다면, 여러분만의 구현을 지정할 수 있고, 심지어 시딩 과정에서 사용할 값 목록도 직접 정의할 수 있습니다.
.refine
은 drizzle-seed
에서 제공하는 모든 생성 함수 목록을 받는 콜백입니다. 이 콜백은 조정하려는 테이블을 키로 가지는 객체를 반환해야 하며, 각 테이블의 동작을 필요에 따라 정의할 수 있습니다. 각 테이블은 데이터베이스 시딩을 단순화하기 위해 여러 속성을 지정할 수 있습니다:
columns
: 각 컬럼의 기본 동작을 조정하기 위해 필요한 생성 함수를 지정합니다.
count
: 데이터베이스에 삽입할 행의 수를 지정합니다. 기본값은 10입니다. seed()
옵션에서 전역 카운트가 정의된 경우, 여기서 정의한 카운트가 해당 테이블에 대해 이를 덮어씁니다.
with
: 연관된 엔티티를 생성하려는 경우, 각 부모 테이블에 대해 생성할 참조 엔티티의 수를 정의합니다.
info
참조 값의 수를 생성할 때 가중치를 적용한 랜덤 분포를 지정할 수도 있습니다. 이 API에 대한 자세한 내용은 가중치 랜덤 문서 를 참조하세요.
API
await seed (db , schema) .refine ((f) => ({
users : {
columns : {} ,
count : 10 ,
with : {
posts : 10
}
} ,
}));
다음은 몇 가지 예제와 함께 어떤 일이 발생하는지 설명합니다:
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
컬럼에 대한 시드 로직을 조정합니다.
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
로의 참조를 생성합니다.
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
엔티티와 연결하지 않습니다. users
의 id
생성을 조정하여 10000
에서 20000
사이의 고유한 정수를 생성하도록 하고, posts
의 값을 직접 정의한 배열에서 가져오도록 조정합니다.
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 패키지에서 가중치 랜덤을 사용할 수 있는 몇 가지 경우는 다음과 같습니다:
각 테이블 내 컬럼의 세부 설정
생성할 관련 엔티티의 수를 결정하는 with
속성
두 가지 예제를 살펴보겠습니다.
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% 확률로 생성되도록 설정합니다.
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% 확률로 생성합니다.
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 ();
복잡한 예제
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 ();
import type { AnyPgColumn } from "drizzle-orm/pg-core" ;
import { integer , numeric , pgTable , text , timestamp , varchar } from "drizzle-orm/pg-core" ;
export const customers = pgTable ( 'customer' , {
id : varchar ({ length : 256 }) .primaryKey () ,
companyName : text () .notNull () ,
contactName : text () .notNull () ,
contactTitle : text () .notNull () ,
address : text () .notNull () ,
city : text () .notNull () ,
postalCode : text () ,
region : text () ,
country : text () .notNull () ,
phone : text () .notNull () ,
fax : text () ,
});
export const employees = pgTable (
'employee' ,
{
id : integer () .primaryKey () ,
lastName : text () .notNull () ,
firstName : text () ,
title : text () .notNull () ,
titleOfCourtesy : text () .notNull () ,
birthDate : timestamp () .notNull () ,
hireDate : timestamp () .notNull () ,
address : text () .notNull () ,
city : text () .notNull () ,
postalCode : text () .notNull () ,
country : text () .notNull () ,
homePhone : text () .notNull () ,
extension : integer () .notNull () ,
notes : text () .notNull () ,
reportsTo : integer () .references (() : AnyPgColumn => employees .id) ,
photoPath : text () ,
} ,
);
export const orders = pgTable ( 'order' , {
id : integer () .primaryKey () ,
orderDate : timestamp () .notNull () ,
requiredDate : timestamp () .notNull () ,
shippedDate : timestamp () ,
shipVia : integer () .notNull () ,
freight : numeric () .notNull () ,
shipName : text () .notNull () ,
shipCity : text () .notNull () ,
shipRegion : text () ,
shipPostalCode : text () ,
shipCountry : text () .notNull () ,
customerId : text () .notNull () .references (() => customers .id , { onDelete : 'cascade' }) ,
employeeId : integer () .notNull () .references (() => employees .id , { onDelete : 'cascade' }) ,
});
export const suppliers = pgTable ( 'supplier' , {
id : integer () .primaryKey () ,
companyName : text () .notNull () ,
contactName : text () .notNull () ,
contactTitle : text () .notNull () ,
address : text () .notNull () ,
city : text () .notNull () ,
region : text () ,
postalCode : text () .notNull () ,
country : text () .notNull () ,
phone : text () .notNull () ,
});
export const products = pgTable ( 'product' , {
id : integer () .primaryKey () ,
name : text () .notNull () ,
quantityPerUnit : text () .notNull () ,
unitPrice : numeric () .notNull () ,
unitsInStock : integer () .notNull () ,
unitsOnOrder : integer () .notNull () ,
reorderLevel : integer () .notNull () ,
discontinued : integer () .notNull () ,
supplierId : integer () .notNull () .references (() => suppliers .id , { onDelete : 'cascade' }) ,
});
export const details = pgTable ( 'order_detail' , {
unitPrice : numeric () .notNull () ,
quantity : integer () .notNull () ,
discount : numeric () .notNull () ,
orderId : integer () .notNull () .references (() => orders .id , { onDelete : 'cascade' }) ,
productId : integer () .notNull () .references (() => products .id , { onDelete : 'cascade' }) ,
});
Limitations
with
옵션의 타입 제한 사항
Drizzle의 현재 API와 TypeScript의 몇 가지 제한 사항 때문에, 특히 테이블 간 순환 참조가 존재할 때 테이블 간의 참조를 올바르게 추론하는 것이 불가능합니다.
이로 인해 with
옵션은 스키마 내의 모든 테이블을 표시하며, 여러분이 일대다 관계를 가진 테이블을 직접 선택해야 합니다.
경고
with
옵션은 일대다 관계에서만 작동합니다. 예를 들어, 하나의 user
와 여러 개의 posts
가 있는 경우, user
와 함께 posts
를 사용할 수 있지만, posts
와 함께 user
를 사용할 수는 없습니다.
Drizzle 테이블의 세 번째 매개변수 타입 제한 사항:
현재 Drizzle 테이블에서 세 번째 매개변수에 대한 타입 지원이 없습니다. 런타임에서는 동작하지만, 타입 레벨에서는 정상적으로 작동하지 않습니다.