名称: bknd-seed-data 描述: 当使用初始或测试数据填充Bknd数据库时使用。涵盖了选项中的seed函数、ctx.em.mutator()用于insertOne/insertMany、条件播种、环境相关数据以及开发/测试装置的常见模式。
种子数据
使用内置的seed函数填充Bknd数据库的初始、测试或开发数据。
先决条件
- Bknd项目已初始化
- 至少定义了一个实体
- 代码优先配置(seed是纯代码的)
何时使用
- 在首次启动时填充初始数据
- 为开发创建测试装置
- 为演示设置示例数据
- 引导管理员用户或默认记录
注意: seed函数是纯代码的—没有UI等效。对于一次性数据输入,直接使用管理面板的数据部分。
代码方法
步骤1:将Seed函数添加到选项
seed函数位于配置的options部分:
import { type BunBkndConfig, serve } from "bknd/adapter/bun";
import { em, entity, text, boolean } from "bknd";
const schema = em({
todos: entity("todos", {
title: text().required(),
done: boolean({ default_value: false }),
}),
});
const config: BunBkndConfig = {
connection: { url: "file:data.db" },
config: {
data: schema.toJSON(),
},
options: {
seed: async (ctx) => {
// 种子逻辑在这里
},
},
};
serve(config);
步骤2:使用ctx.em.mutator()插入数据
使用ctx.em.mutator(entity)进行服务器端插入:
options: {
seed: async (ctx) => {
// 插入单条记录
await ctx.em.mutator("todos").insertOne({
title: "欢迎任务",
done: false,
});
// 插入多条记录
await ctx.em.mutator("todos").insertMany([
{ title: "学习Bknd基础", done: false },
{ title: "创建第一个实体", done: true },
{ title: "设置认证", done: false },
]);
},
}
步骤3:播种相关实体
先插入父记录,然后子记录:
options: {
seed: async (ctx) => {
// 先创建用户
const users = await ctx.em.mutator("users").insertMany([
{ email: "admin@example.com", name: "管理员" },
{ email: "user@example.com", name: "用户" },
]);
// 创建引用用户的帖子
await ctx.em.mutator("posts").insertMany([
{ title: "第一篇帖子", author_id: users[0].id },
{ title: "第二篇帖子", author_id: users[1].id },
]);
},
}
步骤4:条件播种
播种前检查数据是否存在以避免重复:
options: {
seed: async (ctx) => {
// 检查是否已播种
const existing = await ctx.em.repo("users").findOne({
where: { email: { $eq: "admin@example.com" } },
});
if (existing) {
console.log("数据库已播种");
return;
}
// 播种数据
await ctx.em.mutator("users").insertOne({
email: "admin@example.com",
name: "管理员",
});
},
}
完整示例
import { type BunBkndConfig, serve } from "bknd/adapter/bun";
import { em, entity, text, boolean, number, date } from "bknd";
const schema = em({
users: entity("users", {
email: text().required().unique(),
name: text(),
role: text({ default_value: "user" }),
}),
posts: entity("posts", {
title: text().required(),
content: text(),
published: boolean({ default_value: false }),
view_count: number({ default_value: 0 }),
created_at: date({ default_value: "now" }),
}),
tags: entity("tags", {
name: text().required().unique(),
}),
});
type Database = (typeof schema)["DB"];
declare module "bknd" {
interface DB extends Database {}
}
const config: BunBkndConfig = {
connection: { url: "file:data.db" },
config: {
data: schema.toJSON(),
},
options: {
seed: async (ctx) => {
// 检查是否已播种
const count = await ctx.em.repo("users").count();
if (count > 0) {
console.log("跳过播种:数据已存在");
return;
}
console.log("播种数据库...");
// 播种用户
const [admin, author] = await ctx.em.mutator("users").insertMany([
{ email: "admin@example.com", name: "管理员", role: "admin" },
{ email: "author@example.com", name: "作者", role: "author" },
]);
// 播种标签
const tags = await ctx.em.mutator("tags").insertMany([
{ name: "javascript" },
{ name: "typescript" },
{ name: "bknd" },
]);
// 播种帖子
await ctx.em.mutator("posts").insertMany([
{
title: "开始使用Bknd",
content: "学习基础...",
published: true,
author_id: author.id,
},
{
title: "高级模式",
content: "深入探讨...",
published: false,
author_id: admin.id,
},
]);
console.log("播种完成!");
},
},
};
serve(config);
React/浏览器适配器
对于使用BkndBrowserApp的基于浏览器的应用:
import { BkndBrowserApp } from "bknd/adapter/browser";
function App() {
return (
<BkndBrowserApp
config={{
data: schema.toJSON(),
}}
options={{
seed: async (ctx) => {
await ctx.em.mutator("todos").insertMany([
{ title: "示例任务1", done: false },
{ title: "示例任务2", done: true },
]);
},
}}
>
<YourApp />
</BkndBrowserApp>
);
}
环境相关播种
开发与生产的不同数据:
options: {
seed: async (ctx) => {
const isDev = process.env.NODE_ENV !== "production";
// 总是播种管理员
await ctx.em.mutator("users").insertOne({
email: "admin@example.com",
name: "管理员",
role: "admin",
});
// 仅开发测试数据
if (isDev) {
await ctx.em.mutator("users").insertMany([
{ email: "test1@example.com", name: "测试用户1" },
{ email: "test2@example.com", name: "测试用户2" },
]);
// 生成批量测试数据
const testPosts = Array.from({ length: 50 }, (_, i) => ({
title: `测试帖子 ${i + 1}`,
content: `测试帖子 ${i + 1}的内容`,
published: i % 2 === 0,
}));
await ctx.em.mutator("posts").insertMany(testPosts);
}
},
}
种子执行行为
| 场景 | 种子运行? |
|---|---|
| 首次启动(空数据库) | 是 |
| 后续启动 | 是(每次) |
| 模式同步后 | 是 |
| 生产部署 | 是(使用防护!) |
重要: seed函数在每次启动时运行。始终添加存在检查以防止重复数据。
Mutator方法参考
| 方法 | 描述 | 示例 |
|---|---|---|
insertOne(data) |
插入单条记录 | mutator("users").insertOne({ email: "..." }) |
insertMany(data[]) |
插入多条记录 | mutator("users").insertMany([...]) |
常见模式
幂等播种
async function seedIfNotExists(ctx, entity: string, where: object, data: object) {
const existing = await ctx.em.repo(entity).findOne({ where });
if (!existing) {
return ctx.em.mutator(entity).insertOne(data);
}
return existing;
}
// 用法
options: {
seed: async (ctx) => {
await seedIfNotExists(ctx, "users",
{ email: { $eq: "admin@example.com" } },
{ email: "admin@example.com", name: "管理员", role: "admin" }
);
},
}
工厂函数
function createTestUser(overrides = {}) {
return {
email: `user${Date.now()}@test.com`,
name: "测试用户",
role: "user",
...overrides,
};
}
function createTestPost(authorId: number, overrides = {}) {
return {
title: "测试帖子",
content: "Lorem ipsum...",
published: false,
author_id: authorId,
...overrides,
};
}
// 用法
options: {
seed: async (ctx) => {
const user = await ctx.em.mutator("users").insertOne(
createTestUser({ role: "admin" })
);
await ctx.em.mutator("posts").insertMany([
createTestPost(user.id, { title: "帖子1", published: true }),
createTestPost(user.id, { title: "帖子2" }),
]);
},
}
使用Faker数据播种
import { faker } from "@faker-js/faker";
options: {
seed: async (ctx) => {
const users = Array.from({ length: 10 }, () => ({
email: faker.internet.email(),
name: faker.person.fullName(),
role: faker.helpers.arrayElement(["user", "author", "admin"]),
}));
await ctx.em.mutator("users").insertMany(users);
},
}
常见陷阱
重启时的重复数据
问题: 种子每次启动运行,创建重复数据。
修复: 检查现有数据:
seed: async (ctx) => {
const count = await ctx.em.repo("users").count();
if (count > 0) return; // 已播种
// 种子逻辑...
}
外键顺序
问题: 外键约束失败错误。
修复: 先插入父记录再子记录:
// ❌ 错误顺序
await ctx.em.mutator("posts").insertOne({ author_id: 1, ... }); // 用户不存在!
await ctx.em.mutator("users").insertOne({ id: 1, ... });
// ✅ 正确顺序
const user = await ctx.em.mutator("users").insertOne({ ... });
await ctx.em.mutator("posts").insertOne({ author_id: user.id, ... });
缺少必填字段
问题: NOT NULL约束失败错误。
修复: 包括所有必填字段:
// ❌ 缺少必填字段
await ctx.em.mutator("users").insertOne({ name: "管理员" });
// 错误:email是必填的
// ✅ 包括所有必填字段
await ctx.em.mutator("users").insertOne({
email: "admin@example.com", // 必填
name: "管理员"
});
生产中的播种
问题: 测试数据出现在生产环境。
修复: 用环境检查防护:
seed: async (ctx) => {
if (process.env.NODE_ENV === "production") {
// 仅在生产播种必要数据
const adminExists = await ctx.em.repo("users").findOne({
where: { role: { $eq: "admin" } },
});
if (!adminExists) {
await ctx.em.mutator("users").insertOne({
email: process.env.ADMIN_EMAIL,
name: "管理员",
role: "admin",
});
}
return;
}
// 完整的开发种子...
}
验证
播种后,验证数据是否插入:
seed: async (ctx) => {
// ... 插入数据 ...
// 验证
const userCount = await ctx.em.repo("users").count();
const postCount = await ctx.em.repo("posts").count();
console.log(`播种:${userCount} 用户, ${postCount} 帖子`);
}
或通过启动后的API:
const api = app.getApi();
const { data } = await api.data.readMany("users");
console.log("用户:", data.length);
应该做和不应该做
应该做:
- 插入前检查现有数据
- 先插入父记录再子记录(外键顺序)
- 使用环境检查区分开发和生产数据
- 记录播种进度以便调试
- 保持种子函数幂等
不应该做:
- 播种敏感数据(真实密码、API密钥)
- 假设种子只运行一次
- 在代码中硬编码生产管理员凭证
- 跳过必填字段
- 忽略外键关系
相关技能
- bknd-crud-create - 通过API/SDK创建记录
- bknd-bulk-operations - 运行时批量插入/更新/删除
- bknd-create-entity - 播种前定义实体
- bknd-define-relationship - 设置关系以播种链接数据