名称:bknd-定义-关系 描述:在定义Bknd实体间关系时使用。涵盖多对一、一对一、多对多、自引用关系、连接表、选项如mappedBy和inversedBy,以及UI与代码方法。
定义实体关系
在Bknd中创建实体间关系(外键、引用、关联)。
前提条件
- 至少存在两个实体(参见
bknd-创建-实体) - 对于代码模式:访问您的架构文件
关系类型
| 类型 | 使用场景 | 示例 |
|---|---|---|
| 多对一 | 子实体属于一个父实体 | 文章 → 用户(作者) |
| 一对一 | 唯一的1:1配对 | 用户 → 个人资料 |
| 多对多 | 双方都有多个实例 | 文章 ↔ 标签 |
| 自引用 | 实体引用自身 | 类别 → 父类别 |
何时使用UI与代码
使用UI模式时
- 快速原型设计
- 视觉学习者
- 非开发人员设置关系
使用代码模式时
- 需要版本控制
- 可复现的架构
- 自定义选项(mappedBy、connectionTable)
- 团队协作
UI方法
步骤1:访问数据部分
- 启动服务器:
npx bknd run - 打开
http://localhost:1337 - 导航到 数据 部分
步骤2:添加关系字段
- 点击 子 实体(例如,
posts) - 点击 + 添加字段
- 选择 关系 字段类型
- 选择目标实体(例如,
users) - 选择关系类型:
- 多对一:多个文章可以属于一个用户
- 一对一:一个文章恰好有一个用户
- 多对多:文章可以有多个标签,标签可以有多个文章
步骤3:配置选项
- 字段名称:外键的名称(例如,
author创建author_id) - 必需:切换关系是否强制
步骤4:保存和同步
- 点击 保存字段
- 点击 同步数据库 应用更改
代码方法
关系定义在 em() 的第二个参数中:
const schema = em(
{
// 实体定义(第一个参数)
},
({ relation, index }, entities) => {
// 关系定义(第二个参数)
}
);
多对一
子实体属于一个父实体。最常见的关系类型。
import { em, entity, text } from "bknd";
const schema = em(
{
users: entity("users", { email: text().required() }),
posts: entity("posts", { title: text().required() }),
},
({ relation }, { users, posts }) => {
relation(posts).manyToOne(users);
}
);
自动生成: users_id 外键列在 posts 表上
自定义字段名称使用 mappedBy:
({ relation }, { users, posts }) => {
relation(posts).manyToOne(users, {
mappedBy: "author", // 创建 author_id 而不是 users_id
});
}
一对一
唯一的1:1关系。每个子实体恰好属于一个父实体。
const schema = em(
{
users: entity("users", { email: text().required() }),
profiles: entity("profiles", { bio: text() }),
},
({ relation }, { users, profiles }) => {
relation(profiles).oneToOne(users);
}
);
注意: 一对一关系不能使用 $set 操作符(保持唯一性)。
多对多
双方实体都可以有多个对方实例。自动创建连接表。
const schema = em(
{
posts: entity("posts", { title: text().required() }),
tags: entity("tags", { name: text().required() }),
},
({ relation }, { posts, tags }) => {
relation(posts).manyToMany(tags);
}
);
自动生成: posts_tags 连接表,包含 posts_id 和 tags_id 列
自定义连接表名称:
({ relation }, { posts, tags }) => {
relation(posts).manyToMany(tags, {
connectionTable: "post_tags", // 自定义连接表名称
});
}
连接表上的额外字段:
({ relation }, { users, courses }) => {
relation(users).manyToMany(courses, {
connectionTable: "enrollments",
}, {
// 连接表上的额外字段
enrolled_at: date(),
completed: boolean(),
grade: number(),
});
}
自引用
实体引用自身。常用于层次结构(类别、评论、组织图)。
const schema = em(
{
categories: entity("categories", { name: text().required() }),
},
({ relation }, { categories }) => {
relation(categories).manyToOne(categories, {
mappedBy: "parent", // FK 字段:parent_id
inversedBy: "children", // 反向导航
});
}
);
用法:
category.parent_id→ 指向父类别- 查询子项:
api.data.readMany("categories", { where: { parent_id: 5 } })
替代方案:直接外键
代替 relation(),使用数字字段上的 .references():
const schema = em({
users: entity("users", { email: text().required() }),
posts: entity("posts", {
title: text().required(),
author_id: number().references("users.id"),
}),
});
区别: .references() 更简单但不创建反向导航或不支持多对多。
关系选项
ManyToOne / OneToOne 选项
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
mappedBy |
字符串 | 目标实体名称 | FK 字段名称(例如,author → author_id) |
inversedBy |
字符串 | 源实体名称 | 反向导航名称 |
required |
布尔值 | false | 关系是强制的 |
ManyToMany 选项
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
connectionTable |
字符串 | {source}_{target} |
连接表名称 |
查询关系
加载相关数据(with)
const api = app.getApi();
// 加载文章及其作者
const posts = await api.data.readMany("posts", {
with: {
users: { select: ["email", "name"] },
},
});
// 结果:[{ id: 1, title: "...", users: { email: "...", name: "..." } }]
通过关系过滤
// 特定作者的文章
const posts = await api.data.readMany("posts", {
where: { author_id: 5 },
});
// 使用连接进行复杂过滤
const posts = await api.data.readMany("posts", {
join: {
users: { where: { email: "john@example.com" } },
},
});
多对多操作
// 将标签附加到文章
await api.data.updateOne("posts", 1, {
tags: { $attach: [1, 2, 3] }, // 标签ID
});
// 分离标签
await api.data.updateOne("posts", 1, {
tags: { $detach: [2] },
});
// 替换所有标签
await api.data.updateOne("posts", 1, {
tags: { $set: [4, 5] },
});
多对一操作
// 在文章上设置作者
await api.data.updateOne("posts", 1, {
users: { $set: 5 }, // 用户ID
});
常见模式
博客与作者和标签
const schema = em(
{
users: entity("users", {
email: text().required().unique(),
name: text(),
}),
posts: entity("posts", {
title: text().required(),
content: text(),
published: boolean(),
}),
tags: entity("tags", {
name: text().required().unique(),
}),
},
({ relation }, { users, posts, tags }) => {
// 文章有一个作者
relation(posts).manyToOne(users, { mappedBy: "author" });
// 文章有多个标签
relation(posts).manyToMany(tags);
}
);
电商订单
const schema = em(
{
customers: entity("customers", { email: text().required() }),
orders: entity("orders", { total: number() }),
products: entity("products", { name: text().required(), price: number() }),
},
({ relation }, { customers, orders, products }) => {
// 订单属于客户
relation(orders).manyToOne(customers);
// 订单有多个产品(带数量)
relation(orders).manyToMany(products, {
connectionTable: "order_items",
}, {
quantity: number().required(),
unit_price: number().required(),
});
}
);
嵌套类别
const schema = em(
{
categories: entity("categories", {
name: text().required(),
slug: text().required().unique(),
}),
},
({ relation }, { categories }) => {
relation(categories).manyToOne(categories, {
mappedBy: "parent",
inversedBy: "children",
});
}
);
// 用法:获取类别5的所有子项
const children = await api.data.readMany("categories", {
where: { parent_id: 5 },
});
常见陷阱
实体未找到
错误: Entity "user" not found
修复: 实体名称通常为复数。使用 users 而不是 user。
// 错误
relation(posts).manyToOne(user);
// 正确
relation(posts).manyToOne(users);
循环引用错误
错误: Circular dependency detected
修复: 对于自引用,使用适当的选项:
// 正确的自引用
relation(categories).manyToOne(categories, {
mappedBy: "parent",
inversedBy: "children",
});
外键命名冲突
错误: Field "users_id" already exists
修复: 使用 mappedBy 指定不同的字段名称:
// 如果已有 users_id,使用不同的名称
relation(posts).manyToOne(users, { mappedBy: "author" }); // 创建 author_id
一对一关系上的多对多 $set
错误: Cannot use $set on one-to-one relation
修复: 一对一关系通过不同方式保持唯一性。改用 $create:
// 对于一对一
await api.data.updateOne("users", 1, {
profiles: { $create: { bio: "Hello" } },
});
在解构中缺少实体
错误: Cannot read property 'manyToOne' of undefined
修复: 确保实体从第二个回调参数中解构出来:
// 错误 - 解构中缺少 users
({ relation }, { posts }) => {
relation(posts).manyToOne(users); // users 未定义
}
// 正确
({ relation }, { users, posts }) => {
relation(posts).manyToOne(users);
}
关系更改未应用
问题: 添加了关系但没有看到FK列。
修复:
- 重启服务器(架构在启动时同步)
- 验证关系在第二个
em()参数中 - 检查语法错误
验证
检查外键是否创建
npx bknd debug paths
# 在实体输出中查找FK字段
在代码中测试关系
const api = app.getApi();
// 创建父实体
const user = await api.data.createOne("users", { email: "test@example.com" });
// 创建带关系的子实体
const post = await api.data.createOne("posts", {
title: "测试文章",
author_id: user.data.id,
});
// 加载带关系
const loaded = await api.data.readOne("posts", post.data.id, {
with: { users: true },
});
console.log(loaded.data.users); // { id: 1, email: "test@example.com" }
做与不做
做:
- 使用复数实体名称(
users,posts) - 使用
mappedBy获得语义化字段名称(author而不是users) - 在第二个
em()参数中定义关系 - 使用
.references()用于无需导航的简单FK
不做:
- 在关系中使用单数实体名称
- 在使用
relation()时创建手动FK字段(它会自动创建) - 在一对一关系上使用
$set - 忘记在回调中解构实体
相关技能
- bknd-创建-实体 - 在定义关系前创建实体
- bknd-添加-字段 - 添加字段包括用于简单FK的
.references() - bknd-CRUD-读取 - 使用
with和join查询相关数据 - bknd-CRUD-更新 - 使用
$attach,$detach,$set进行关系更新 - bknd-查询-过滤 - 关系上的高级过滤