种子数据Skill bknd-seed-data

这个技能用于在Bknd数据库中播种初始或测试数据,通过内置的seed函数实现条件播种、环境相关数据、工厂函数等模式,适用于开发、测试和演示场景,确保数据库拥有必要数据。关键词包括Bknd、数据库播种、种子数据、开发测试数据、条件逻辑、幂等播种、环境相关播种。

后端开发 0 次安装 0 次浏览 更新于 3/8/2026

名称: 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 - 设置关系以播种链接数据