Drizzle Seed

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

什么是确定性数据生成?

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

伪随机数生成器(pRNG)

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

使用 pRNG 的好处:

使用 drizzle-seed,你可以两全其美:既能生成逼真的假数据,又能在需要时控制并复现它。

安装

npm
yarn
pnpm
bun
npm i drizzle-seed

基本用法

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

import { mysqlTable, int, varchar } from "drizzle-orm/mysql-core";
import { drizzle } from "drizzle-orm/mysql2";
import { seed } from "drizzle-seed";

const users = mysqlTable("users", {
  id: int().primaryKey().autoincrement(),
  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 来为后续所有运行生成不同的一组值,你可以在 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();

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

对于 MySQL,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 的所有可用生成器函数列表。它应该返回一个对象,其中的键表示你想要精细化的表,并按需定义它们的行为。
每个表都可以指定几个属性,以简化数据库播种:

info

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

API

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

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

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

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

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

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

index.ts
import { drizzle } from "drizzle-orm/mysql2";
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/mysql2";
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/mysql2";
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 实体关联。为 usersid 生成进行精细化,使其返回任意从 1000020000 的 int 且保持唯一,并将 posts 精细化为从自定义数组中获取值

index.ts
import { drizzle } from "drizzle-orm/mysql2";
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 可以生成的相关实体数量。

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

加权随机

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

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

下面我们来看两个示例:

schema.ts
import { mysqlTable, int, varchar, double } from "drizzle-orm/mysql-core";

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

export const details = mysqlTable(
  "details",
  {
    unitPrice: double().notNull(),
    quantity: int().notNull(),
    discount: double().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/mysql2";
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:对于每个订单,生成 1 到 3 条 details 的概率为 60%,生成 5 到 7 条 details 的概率为 30%,生成 8 到 10 条 details 的概率为 10%

index.ts
import { drizzle } from "drizzle-orm/mysql2";
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 表第三个参数的类型。虽然它在运行时可以正常工作,但在类型层面上不会正确生效