bknd-seed-data

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Seed 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
options
section of your config:
typescript
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函数位于配置的
options
部分:
typescript
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
ctx.em.mutator(entity)
for server-side inserts:
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
BkndBrowserApp
:
tsx
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>
  );
}
对于使用
BkndBrowserApp
的浏览器端应用:
tsx
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

种子数据执行行为

ScenarioSeed Runs?
First startup (empty DB)Yes
Subsequent startupsYes (every time)
After schema syncYes
Production deploymentYes (use guards!)
Important: The seed function runs on every startup. Always add existence checks to prevent duplicate data.
场景是否执行种子数据填充?
首次启动(数据库为空)
后续启动
Schema同步后
生产环境部署是(务必添加防护逻辑!)
重要提示: seed函数会在每次启动时执行。务必添加数据存在性检查,避免重复创建数据。

Mutator Methods Reference

Mutator方法参考

MethodDescriptionExample
insertOne(data)
Insert single record
mutator("users").insertOne({ email: "..." })
insertMany(data[])
Insert multiple records
mutator("users").insertMany([...])
方法说明示例
insertOne(data)
插入单条记录
mutator("users").insertOne({ email: "..." })
insertMany(data[])
插入多条记录
mutator("users").insertMany([...])

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:
Foreign key constraint failed
error.
Fix: 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:
NOT NULL constraint failed
error.
Fix: 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 - 为关联数据填充设置关系