bknd-seed-data
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSeed Data
种子数据填充
Populate your Bknd database with initial, test, or development data using the built-in seed function.
使用内置的seed函数为你的Bknd数据库填充初始数据、测试数据或开发环境数据。
Prerequisites
前置条件
- Bknd project initialized
- At least one entity defined
- Code-first configuration (seed is code-only)
- 已初始化Bknd项目
- 至少定义了一个实体
- 采用代码优先的配置方式(seed仅支持代码实现)
When to Use
适用场景
- Populating initial data on first startup
- Creating test fixtures for development
- Setting up demo data for presentations
- Bootstrapping admin users or default records
Note: Seed function is code-only—no UI equivalent. For one-off data entry, use the admin panel Data section directly.
- 首次启动时填充初始数据
- 为开发环境创建测试用例
- 为演示准备示例数据
- 快速创建管理员用户或默认记录
注意: seed函数仅支持代码实现,没有对应的UI操作。如果是一次性数据录入,请直接使用管理面板的Data板块。
Code Approach
代码实现步骤
Step 1: Add Seed Function to Options
步骤1:在配置选项中添加Seed函数
The seed function lives in the section of your config:
optionstypescript
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) => {
// Seed logic here
},
},
};
serve(config);seed函数位于配置的部分:
optionstypescript
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);Step 2: Insert Data with ctx.em.mutator()
步骤2:使用ctx.em.mutator()插入数据
Use for server-side inserts:
ctx.em.mutator(entity)typescript
options: {
seed: async (ctx) => {
// Insert single record
await ctx.em.mutator("todos").insertOne({
title: "Welcome task",
done: false,
});
// Insert multiple records
await ctx.em.mutator("todos").insertMany([
{ title: "Learn Bknd basics", done: false },
{ title: "Create first entity", done: true },
{ title: "Set up authentication", done: false },
]);
},
}使用执行服务端数据插入:
ctx.em.mutator(entity)typescript
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 },
]);
},
}Step 3: Seed Related Entities
步骤3:填充关联实体数据
Insert parent records first, then children:
typescript
options: {
seed: async (ctx) => {
// Create users first
const users = await ctx.em.mutator("users").insertMany([
{ email: "admin@example.com", name: "Admin" },
{ email: "user@example.com", name: "User" },
]);
// Create posts referencing users
await ctx.em.mutator("posts").insertMany([
{ title: "First Post", author_id: users[0].id },
{ title: "Second Post", author_id: users[1].id },
]);
},
}先插入父级记录,再插入子级记录:
typescript
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 },
]);
},
}Step 4: Conditional Seeding
步骤4:条件式数据填充
Check if data exists before seeding to avoid duplicates:
typescript
options: {
seed: async (ctx) => {
// Check if already seeded
const existing = await ctx.em.repo("users").findOne({
where: { email: { $eq: "admin@example.com" } },
});
if (existing) {
console.log("Database already seeded");
return;
}
// Seed data
await ctx.em.mutator("users").insertOne({
email: "admin@example.com",
name: "Admin",
});
},
}在填充前检查数据是否已存在,避免重复:
typescript
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: "管理员",
});
},
}Full Example
完整示例
typescript
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) => {
// Check if already seeded
const count = await ctx.em.repo("users").count();
if (count > 0) {
console.log("Skipping seed: data exists");
return;
}
console.log("Seeding database...");
// Seed users
const [admin, author] = await ctx.em.mutator("users").insertMany([
{ email: "admin@example.com", name: "Admin", role: "admin" },
{ email: "author@example.com", name: "Author", role: "author" },
]);
// Seed tags
const tags = await ctx.em.mutator("tags").insertMany([
{ name: "javascript" },
{ name: "typescript" },
{ name: "bknd" },
]);
// Seed posts
await ctx.em.mutator("posts").insertMany([
{
title: "Getting Started with Bknd",
content: "Learn the basics...",
published: true,
author_id: author.id,
},
{
title: "Advanced Patterns",
content: "Deep dive into...",
published: false,
author_id: admin.id,
},
]);
console.log("Seed complete!");
},
},
};
serve(config);typescript
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/Browser Adapter
React/浏览器适配器
For browser-based apps using :
BkndBrowserApptsx
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: "Sample task 1", done: false },
{ title: "Sample task 2", done: true },
]);
},
}}
>
<YourApp />
</BkndBrowserApp>
);
}对于使用的浏览器端应用:
BkndBrowserApptsx
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 },
]);
},
}}
>
<你的应用组件 />
</BkndBrowserApp>
);
}Environment-Based Seeding
基于环境的差异化数据填充
Different data for dev vs production:
typescript
options: {
seed: async (ctx) => {
const isDev = process.env.NODE_ENV !== "production";
// Always seed admin
await ctx.em.mutator("users").insertOne({
email: "admin@example.com",
name: "Admin",
role: "admin",
});
// Dev-only test data
if (isDev) {
await ctx.em.mutator("users").insertMany([
{ email: "test1@example.com", name: "Test User 1" },
{ email: "test2@example.com", name: "Test User 2" },
]);
// Generate bulk test data
const testPosts = Array.from({ length: 50 }, (_, i) => ({
title: `Test Post ${i + 1}`,
content: `Content for test post ${i + 1}`,
published: i % 2 === 0,
}));
await ctx.em.mutator("posts").insertMany(testPosts);
}
},
}为开发环境和生产环境配置不同的填充数据:
typescript
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 Execution Behavior
种子数据执行行为
| Scenario | Seed Runs? |
|---|---|
| First startup (empty DB) | Yes |
| Subsequent startups | Yes (every time) |
| After schema sync | Yes |
| Production deployment | Yes (use guards!) |
Important: The seed function runs on every startup. Always add existence checks to prevent duplicate data.
| 场景 | 是否执行种子数据填充? |
|---|---|
| 首次启动(数据库为空) | 是 |
| 后续启动 | 是 |
| Schema同步后 | 是 |
| 生产环境部署 | 是(务必添加防护逻辑!) |
重要提示: seed函数会在每次启动时执行。务必添加数据存在性检查,避免重复创建数据。
Mutator Methods Reference
Mutator方法参考
| Method | Description | Example |
|---|---|---|
| Insert single record | |
| Insert multiple records | |
| 方法 | 说明 | 示例 |
|---|---|---|
| 插入单条记录 | |
| 插入多条记录 | |
Common Patterns
常见实现模式
Idempotent Seeding
幂等性数据填充
typescript
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;
}
// Usage
options: {
seed: async (ctx) => {
await seedIfNotExists(ctx, "users",
{ email: { $eq: "admin@example.com" } },
{ email: "admin@example.com", name: "Admin", role: "admin" }
);
},
}typescript
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" }
);
},
}Factory Functions
工厂函数模式
typescript
function createTestUser(overrides = {}) {
return {
email: `user${Date.now()}@test.com`,
name: "Test User",
role: "user",
...overrides,
};
}
function createTestPost(authorId: number, overrides = {}) {
return {
title: "Test Post",
content: "Lorem ipsum...",
published: false,
author_id: authorId,
...overrides,
};
}
// Usage
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: "Post 1", published: true }),
createTestPost(user.id, { title: "Post 2" }),
]);
},
}typescript
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" }),
]);
},
}Seeding with Faker Data
使用Faker生成模拟数据
typescript
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);
},
}typescript
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);
},
}Common Pitfalls
常见问题
Duplicate Data on Restart
重启后数据重复
Problem: Seed runs every startup, creating duplicates.
Fix: Check for existing data:
typescript
seed: async (ctx) => {
const count = await ctx.em.repo("users").count();
if (count > 0) return; // Already seeded
// Seed logic...
}问题: 每次启动都会执行seed,导致数据重复。
解决方法: 检查数据是否已存在:
typescript
seed: async (ctx) => {
const count = await ctx.em.repo("users").count();
if (count > 0) return; // 已完成数据填充
// 种子数据逻辑...
}Foreign Key Order
外键约束错误
Problem: error.
Foreign key constraint failedFix: Insert parent records before children:
typescript
// ❌ Wrong order
await ctx.em.mutator("posts").insertOne({ author_id: 1, ... }); // User doesn't exist!
await ctx.em.mutator("users").insertOne({ id: 1, ... });
// ✅ Correct order
const user = await ctx.em.mutator("users").insertOne({ ... });
await ctx.em.mutator("posts").insertOne({ author_id: user.id, ... });问题: 出现错误。
Foreign key constraint failed解决方法: 先插入父级记录,再插入子级记录:
typescript
// ❌ 错误顺序
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, ... });Missing Required Fields
缺少必填字段
Problem: error.
NOT NULL constraint failedFix: Include all required fields:
typescript
// ❌ Missing required field
await ctx.em.mutator("users").insertOne({ name: "Admin" });
// Error: email is required
// ✅ Include all required fields
await ctx.em.mutator("users").insertOne({
email: "admin@example.com", // required
name: "Admin"
});问题: 出现错误。
NOT NULL constraint failed解决方法: 包含所有必填字段:
typescript
// ❌ 缺少必填字段
await ctx.em.mutator("users").insertOne({ name: "管理员" });
// 错误:email为必填项
// ✅ 包含所有必填字段
await ctx.em.mutator("users").insertOne({
email: "admin@example.com", // 必填
name: "管理员"
});Seed in Production
生产环境执行种子数据填充
Problem: Test data appears in production.
Fix: Guard with environment check:
typescript
seed: async (ctx) => {
if (process.env.NODE_ENV === "production") {
// Only seed essential data in 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: "Admin",
role: "admin",
});
}
return;
}
// Full dev seed...
}问题: 测试数据出现在生产环境中。
解决方法: 通过环境检查进行防护:
typescript
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;
}
// 开发环境完整数据填充...
}Verification
验证数据
After seeding, verify data was inserted:
typescript
seed: async (ctx) => {
// ... insert data ...
// Verify
const userCount = await ctx.em.repo("users").count();
const postCount = await ctx.em.repo("posts").count();
console.log(`Seeded: ${userCount} users, ${postCount} posts`);
}Or via API after startup:
typescript
const api = app.getApi();
const { data } = await api.data.readMany("users");
console.log("Users:", data.length);填充完成后,验证数据是否成功插入:
typescript
seed: async (ctx) => {
// ... 插入数据 ...
// 验证
const userCount = await ctx.em.repo("users").count();
const postCount = await ctx.em.repo("posts").count();
console.log(`已填充:${userCount}个用户,${postCount}篇帖子`);
}或者在启动后通过API验证:
typescript
const api = app.getApi();
const { data } = await api.data.readMany("users");
console.log("用户数量:", data.length);DOs and DON'Ts
注意事项
DO:
- Check for existing data before inserting
- Insert parent records before children (FK order)
- Use environment checks for dev vs prod data
- Log seed progress for debugging
- Keep seed functions idempotent
DON'T:
- Seed sensitive data (real passwords, API keys)
- Assume seed runs only once
- Hardcode production admin credentials in code
- Skip required fields
- Ignore foreign key relationships
建议:
- 插入前检查数据是否已存在
- 按外键顺序插入(先父后子)
- 使用环境检查区分开发与生产数据
- 记录种子数据填充进度以便调试
- 确保seed函数具备幂等性
禁止:
- 填充敏感数据(真实密码、API密钥)
- 假设seed仅执行一次
- 在代码中硬编码生产环境管理员凭证
- 遗漏必填字段
- 忽略外键关联关系
Related Skills
相关技能
- bknd-crud-create - Create records via API/SDK
- bknd-bulk-operations - Bulk insert/update/delete at runtime
- bknd-create-entity - Define entities before seeding
- bknd-define-relationship - Set up relations for seeding linked data
- bknd-crud-create - 通过API/SDK创建记录
- bknd-bulk-operations - 运行时批量插入/更新/删除
- bknd-create-entity - 填充数据前定义实体
- bknd-define-relationship - 为关联数据填充设置关系