Drizzle Seed

drizzle-seed 是一个 TypeScript 库,帮助你生成确定性但又逼真的假数据,用于填充你的数据库。通过利用可设种子的伪随机数生成器(pRNG),它确保你生成的数据在不同运行之间保持一致且可复现。这对于测试、开发和调试尤其有用。

什么是确定性数据生成?

确定性数据生成意味着相同的输入总会产生相同的输出。以 drizzle-seed 为例,当你使用相同的种子数字初始化该库时,它每次都会生成相同的假数据序列。这使得数据集具有可预测性和可重复性。

伪随机数生成器(pRNG)

伪随机数生成器是一种算法,它生成的数字序列在特性上近似随机数。然而,因为它基于一个称为种子的初始值,你可以控制它的随机性。使用相同的种子,pRNG 将产生相同的数字序列,使你的数据生成过程具备可复现性。

使用 pRNG 的好处:

使用 drizzle-seed,你可以同时获得两者的最佳体验:既能生成逼真的假数据,又能在需要时随时将其复现。

安装

npm
yarn
pnpm
bun
npm i drizzle-seed

基本用法

在这个示例中,我们将创建 10 个具有随机姓名和 id 的用户

import { mssqlTable, int, varchar } from "drizzle-orm/mssql-core";
import { drizzle } from "drizzle-orm/node-mssql";
import { seed } from "drizzle-seed";

const users = mssqlTable("users", {
  id: int().primaryKey().identity(),
  name: varchar({ length: 255 }).notNull(),
});

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

main();

选项

count

默认情况下,seed 函数会创建 10 个实体。 不过,如果你的测试需要更多实体,可以在 seed 选项对象中指定这一项

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

seed

如果你需要一个种子来为后续所有运行生成一组不同的值,可以在 seed 选项中定义一个不同的数字。 任何新的数字都会生成一组唯一的值

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

重置数据库

使用 drizzle-seed,你可以轻松重置数据库并用新值进行填充,例如在测试套件中

// 要重置的 schema 文件路径
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();

不同的方言会有不同的数据库重置策略。对于 MSSQL:

对于 MSSQL,drizzle-seed 包会先禁用 FOREIGN_KEY_CHECKS,以确保下一步不会失败,然后生成 TRUNCATE 语句来清空所有表中的内容

SET FOREIGN_KEY_CHECKS = 0;
TRUNCATE tableName1;
TRUNCATE tableName2;
...
SET FOREIGN_KEY_CHECKS = 1;

精细化

如果你需要更改 drizzle-seed 默认使用的种子生成器函数行为,你可以指定自己的实现,甚至在播种过程中使用你自己的值列表

.refine 是一个回调函数,它接收来自 drizzle-seed 的所有可用生成器函数列表。它应返回一个对象,其键表示你想要精细化的表,并按需定义它们的行为。 每个表都可以指定几个属性来简化数据库播种:

信息

你也可以为你想要创建的引用值数量指定加权随机分布。有关此 API 的详细信息,你可以参考 加权随机文档 文档部分

API

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

让我们看几个示例,并解释会发生什么:

schema.ts
import { mssqlTable, int, varchar } from "drizzle-orm/mssql-core";

export const users = mssqlTable("users", {
  id: int().primaryKey().identity(),
  name: varchar({ length: 255 }).notNull(),
});

export const posts = mssqlTable("posts", {
  id: int().primaryKey().identity(),
  description: varchar({ length: 255 }),
  userId: int().references(() => users.id),
});

示例 1:仅为 users 表播种 20 条实体,并对 name 列使用精细化的种子逻辑

index.ts
import { drizzle } from "drizzle-orm/node-mssql";
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 表播种,并将 name 列排除在播种之外

index.ts
import { drizzle } from "drizzle-orm/node-mssql";
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: false, // name 列将不会被播种,从而允许数据库使用其默认值。
        }
    }
  }));
}

main();

示例 3:为 users 表播种 20 条实体,并通过为 posts 表播种并创建从 postsusers 的引用,为每个 user 添加 10 条 posts

index.ts
import { drizzle } from "drizzle-orm/node-mssql";
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();

示例 4:为 users 表播种 5 条实体,并向数据库中填充 100 条 posts,而不将它们与 users 实体关联。为 users 精细化 id 生成,使其生成任何从 1000020000 的 int,并保持唯一,同时为 posts 精细化为从自定义数组中获取值

index.ts
import { drizzle } from "drizzle-orm/node-mssql";
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: [
                "太阳落在群山之后,将天空染成橙色和紫色的色调", 
                "我简直不敢相信这次自制披萨竟然做得这么好吃!", 
                "有时候,你所需要的只是一本文字优美的书和一个安静的角落。", 
                "还有谁觉得下雨天特别适合狂看老电影?", 
                "今天尝试了一条新的徒步路线,竟然发现了最令人惊叹的瀑布!",
                // ...
            ],
          })
        }
    }
  }));
}

main();
IMPORTANT

我们将在这些文档中定义更多可能性,但目前你可以浏览本文档中的几个部分。查看 生成器 部分,以熟悉你可以使用的所有可用生成器函数。

一个特别棒的功能是能够使用加权随机化,既可用于为列创建的生成器值,也可用于确定 drizzle-seed 可以生成的关联实体数量。

更多信息请查看 加权随机文档

加权随机

在某些情况下,你可能需要在 seed 阶段将多个具有不同优先级的数据集插入到数据库中。对于这种情况,drizzle-seed 提供了一个名为 weighted random 的 API

Drizzle Seed 包有几个可以使用加权随机的地方:

我们来看两个示例:

schema.ts
import { mssqlTable, int, varchar, numeric } from "drizzle-orm/mssql-core";

export const orders = mssqlTable(
  "orders",
  {
    id: int().primaryKey().identity(),
    name: varchar({ length: 255 }).notNull(),
    quantityPerUnit: varchar().notNull(),
    unitPrice: numeric().notNull(),
    unitsInStock: int().notNull(),
    unitsOnOrder: int().notNull(),
    reorderLevel: int().notNull(),
    discontinued: int().notNull(),
  }
);

export const details = mssqlTable(
  "details",
  {
    unitPrice: numeric().notNull(),
    quantity: int().notNull(),
    discount: numeric().notNull(),

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

示例 1:精细化配置 unitPrice 的生成逻辑,生成 5000 个随机价格,其中有 30% 的概率生成 10-100 之间的价格,70% 的概率生成 100-300 之间的价格

index.ts
import { drizzle } from "drizzle-orm/node-mssql";
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: f.int({ minValue: 10, maxValue: 100 })
                   },
                   {
                       weight: 0.7,
                       value: f.number({ minValue: 100, maxValue: 300, precision: 100 })
                   }
               ]
           ),
       }
    }
  }));
}

main();

示例 2:对于每个订单,生成 1 到 3 条 details 的概率为 60%,生成 5 到 7 条 details 的概率为 30%,生成 8 到 10 条 details 的概率为 10%

index.ts
import { drizzle } from "drizzle-orm/node-mssql";
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();

限制

with 的类型限制

由于某些 TypeScript 的限制以及 Drizzle 当前的 API,无法正确推断表之间的引用关系,尤其是在表之间存在循环依赖时。

这意味着 with 选项会显示 schema 中的所有表,你需要手动选择其中具有一对多关系的表

warning

with 选项适用于一对多关系。例如,如果你有一个 user 和多个 posts,你可以使用 users with posts,但不能使用 posts with users

Drizzle 表中第三个参数的类型限制:

目前,我们不支持 Drizzle 表中第三个参数的类型。虽然它在运行时可以工作,但在类型层面上无法正常生效