跳到主要内容

关联关系详解

关联关系是指数据模型之间的连接关系,通过关联字段将不同模型的数据关联起来,实现数据之间的逻辑连接。

新建关联关系时,系统会在两个数据模型中分别建立关联字段,用于存储关联数据的 _id

⚠️ 注意:MySQL 数据模型中的关联关系字段是通过中间表进行关联的,因此不能直接通过 SQL 的 JOIN 语句进行查询。需要使用数据模型提供的关联查询方法来获取关联数据。

常见的关联关系场景:

  • 学生属于某个班级(学生 → 班级)

    • 学生模型关联字段:所属班级
    • 班级模型关联字段:学生列表
  • 文章有多个评论(文章 → 评论)

    • 文章模型关联字段:评论列表
    • 评论模型关联字段:所属文章
  • 用户有一个个人资料(用户 → 个人资料)

    • 用户模型关联字段:个人资料
    • 个人资料模型关联字段:所属用户

支持的数据库类型

数据库类型支持的关联关系
数据库(文档型)一对一、一对多、多对一
数据库(MySQL)一对一、一对多、多对一、多对多
自有 MySQL 数据库一对一、一对多、多对一、多对多

关联关系类型

一对一(1:1)

一个记录只能关联另一个模型的一个记录,双方都是唯一对应关系。

示例: 用户 ↔ 个人资料

// 用户模型
{
name: "张三",
profile: {
_id: "profile_123"
}
}

// 个人资料模型
{
_id: "profile_123",
avatar: "avatar.jpg",
bio: "个人简介"
}

一对多(1:N)

一个记录可以关联另一个模型的多个记录。

示例: 班级 ↔ 学生

// 班级模型
{
name: "一年级1班",
students: [
{ _id: "student_1" },
{ _id: "student_2" }
]
}

多对一(N:1)

多个记录关联另一个模型的一个记录。

示例: 学生 ↔ 班级

// 学生模型
{
name: "小明",
class: {
_id: "class_123"
}
}

多对多(M:N)

多个记录可以关联另一个模型的多个记录。

示例: 学生 ↔ 课程

// 学生模型
{
name: "小明",
courses: [
{ _id: "course_1" }, // 语文
{ _id: "course_2" } // 数学
]
}

关联关系操作

在使用关联关系时,需要注意数据格式和错误处理。无论是在客户端还是服务端(云函数)环境中,操作方式基本一致。

💡 注意:所有关联字段操作都需要使用 {_id: "xxx"} 格式,其中 xxx 为关联数据的 _id,无需传入其他字段。

查询操作

💡 注意:对于 MySQL 数据库,关联查询必须使用数据模型提供的查询方法,不支持直接使用 SQL JOIN 语句。

⚠️ 重要限制:关联查询目前只支持 一层关联,不支持多层嵌套的关联查询。

关联查询层级限制说明

支持的查询(一层关联)

// ✅ 正确:查询文章及其关联的作者信息(一层关联)
const { data } = await models.post.get({
filter: {
where: { _id: { $eq: "post_123" } }
},
select: {
_id: true,
title: true,
author: { // 关联字段:一层
_id: true,
name: true,
email: true,
profile: true // 非关联字段,可以查询
}
}
});

不支持的查询(多层关联)

// ❌ 错误:多层嵌套关联查询不生效
const { data } = await models.post.get({
filter: {
where: { _id: { $eq: "post_123" } }
},
select: {
title: true,
author: { // 第一层关联
name: true,
profile: { // ❌ 第二层关联:不支持
address: { // ❌ 第三层关联:不支持
city: true
}
}
}
}
});
// 查询结果中 profile.address 将不会返回数据

区分关联字段和普通字段

// ✅ 正确:在关联字段中查询普通字段(非关联字段)
const { data } = await models.post.get({
filter: {
where: { _id: { $eq: "post_123" } }
},
select: {
title: true,
author: { // 关联字段(一层)
name: true,
email: true,
avatar: true, // 普通字段
bio: true // 普通字段
// 以上都是 author 模型的普通字段,不是关联字段
}
}
});

多层关联的替代方案

如果需要查询多层关联数据,需要分步查询:

// 方案1:分步查询
// 第一步:查询文章和作者
const post = await models.post.get({
filter: { where: { _id: { $eq: "post_123" } } },
select: {
title: true,
author: {
_id: true,
name: true,
profileId: true // 获取 profile 的 ID
}
}
});

// 第二步:根据 profileId 查询 profile 详情
if (post.data.author.profileId) {
const profile = await models.profile.get({
filter: { where: { _id: { $eq: post.data.author.profileId } } },
select: {
address: {
city: true,
street: true
}
}
});

// 手动组合数据
post.data.author.profile = profile.data;
}
// 方案2:数据冗余
// 在 author 模型中冗余常用的 profile 信息
{
_id: "author_123",
name: "张三",
email: "zhang@example.com",
profileId: "profile_456", // 关联字段
city: "北京", // 冗余字段:来自 profile.address.city
bio: "个人简介" // 冗余字段:来自 profile.bio
}

// 这样可以在一层查询中获取所需信息
const { data } = await models.post.get({
filter: { where: { _id: { $eq: "post_123" } } },
select: {
title: true,
author: {
name: true,
city: true, // 冗余的 city 字段
bio: true // 冗余的 bio 字段
}
}
});

查询结果包含关联数据

// 查询文章及其评论(一层关联)
const { data } = await models.post.get({
filter: {
where: {
_id: { $eq: "post_123" }
}
},
select: {
_id: true,
title: true,
content: true,
// 包含关联的评论数据(一层关联)
comments: {
_id: true,
content: true,
createdAt: true
}
}
});

根据关联条件过滤

// 查询有评论的文章
const { data } = await models.post.list({
filter: {
relateWhere: {
comments: {
where: {
content: { $nempty: true }
}
}
}
},
select: {
_id: true,
title: true,
comments: {
content: true
}
}
});

创建操作

创建记录时可以同时建立关联关系:

// 创建学生并关联班级和课程
const { data } = await models.student.create({
data: {
name: "小明",
age: 8,
// 关联班级(多对一)
class: {
_id: "class_123"
},
// 关联多个课程(多对多)
courses: [
{ _id: "course_1" },
{ _id: "course_2" }
]
}
});

更新操作

更新一对一关联

// 客户端/云函数:更新用户的个人资料关联
const { data } = await models.user.update({
filter: {
where: {
_id: { $eq: "user_123" }
}
},
data: {
profile: {
_id: "profile_456"
}
}
});
// 云函数中使用 doc() 方法的写法
exports.main = async (event) => {
const { userId, profileId } = event;

try {
const result = await cloudbase.model('user').doc(userId).update({
profile: {
_id: profileId
}
});

return { success: true, result };
} catch (error) {
if (error.code === 'INVALID_RELATION_FORMAT') {
return {
success: false,
message: '关联字段格式错误,请使用 {_id: "xxx"} 格式'
};
}
throw error;
}
};

更新一对多关联

// 客户端/云函数:更新班级的学生列表
const { data } = await models.class.update({
filter: {
where: {
_id: { $eq: "class_123" }
}
},
data: {
students: [
{ _id: "student_1" },
{ _id: "student_2" },
{ _id: "student_3" }
]
}
});
// 云函数示例:从参数批量处理
exports.main = async (event) => {
const { classId, studentIds } = event;

try {
const result = await cloudbase.model('class').doc(classId).update({
students: studentIds.map(id => ({ _id: id }))
});

return { success: true, result };
} catch (error) {
if (error.code === 'INVALID_RELATION_FORMAT') {
return {
success: false,
message: '关联字段格式错误,数组中每项需要使用 {_id: "xxx"} 格式'
};
}
throw error;
}
};

更新多对多关联

// 客户端/云函数:更新学生选课
const { data } = await models.student.update({
filter: {
where: {
_id: { $eq: "student_123" }
}
},
data: {
courses: [
{ _id: "course_1" },
{ _id: "course_2" }
]
}
});
// 云函数示例:更新学生选课
exports.main = async (event) => {
const { studentId, courseIds } = event;

try {
const result = await cloudbase.model('student').doc(studentId).update({
courses: courseIds.map(id => ({ _id: id }))
});

return { success: true, result };
} catch (error) {
if (error.code === 'INVALID_RELATION_FORMAT') {
return {
success: false,
message: '关联字段格式错误,请使用 {_id: "xxx"} 格式的数组'
};
}
throw error;
}
};

添加关联数据

向已有的关联列表中添加新的关联记录:

// 向已有学生添加新课程
// 第一步:查询当前课程列表
const student = await models.student.get({
filter: {
where: { _id: { $eq: "student_123" } }
},
select: {
courses: true
}
});

const currentCourses = student.data.courses || [];

// 第二步:检查是否已存在,避免重复
const courseExists = currentCourses.some(c => c._id === "course_3");
if (!courseExists) {
// 第三步:更新课程列表
await models.student.update({
filter: {
where: { _id: { $eq: "student_123" } }
},
data: {
courses: [
...currentCourses,
{ _id: "course_3" }
]
}
});
}
// 云函数封装示例
exports.main = async (event) => {
const { studentId, newCourseId } = event;

try {
// 先查询当前课程列表
const student = await cloudbase.model('student').doc(studentId).get();
const currentCourses = student.data.courses || [];

// 添加新课程(避免重复)
const courseExists = currentCourses.some(c => c._id === newCourseId);
if (courseExists) {
return { success: false, message: '课程已存在' };
}

// 更新课程列表
const result = await cloudbase.model('student').doc(studentId).update({
courses: [
...currentCourses,
{ _id: newCourseId }
]
});

return { success: true, result };
} catch (error) {
console.error('添加课程失败', error);
throw error;
}
};

移除关联数据

从关联列表中移除指定的关联记录:

// 移除学生的某门课程
// 第一步:查询当前课程列表
const student = await models.student.get({
filter: {
where: { _id: { $eq: "student_123" } }
},
select: {
courses: true
}
});

const currentCourses = student.data.courses || [];

// 第二步:过滤掉要移除的课程
const updatedCourses = currentCourses.filter(c => c._id !== "course_2");

// 第三步:更新课程列表
await models.student.update({
filter: {
where: { _id: { $eq: "student_123" } }
},
data: {
courses: updatedCourses
}
});
// 云函数封装示例
exports.main = async (event) => {
const { studentId, courseId } = event;

try {
// 先查询当前课程列表
const student = await cloudbase.model('student').doc(studentId).get();
const currentCourses = student.data.courses || [];

// 过滤掉要移除的课程
const updatedCourses = currentCourses.filter(c => c._id !== courseId);

// 更新课程列表
const result = await cloudbase.model('student').doc(studentId).update({
courses: updatedCourses
});

return { success: true, result };
} catch (error) {
console.error('移除课程失败', error);
throw error;
}
};

删除操作

配置关联关系时,可以设置不同的删除行为:

  • 删除关联模型数据:删除主记录时,同时删除关联的记录
  • 不删除关联模型数据:仅删除主记录,保留关联记录
  • 禁止删除存在关联关系的数据:如果存在关联记录,则禁止删除主记录

关联字段限制说明

字段长度限制

⚠️ 注意:关联字段存储的是 _id 值,存在以下长度限制:

限制项最大长度说明
单个 _id256 字节单个关联记录的 ID 长度限制
一对多/多对多数组1024 字节存储多个 _id 的数组总长度限制
图片 CloudID100-200+ 字节云存储图片 ID 可能较长,需特别注意

多对多关联数量限制

对于多对多关系,由于总长度限制为 1024 字节,关联记录数量受到限制:

单个 _id 长度最大关联数量说明
20 字节约 50 个MongoDB ObjectId 标准长度
36 字节约 28 个UUID 格式
10 字节约 100 个自定义短 ID

建议

  • 对于需要大量关联的场景(如用户收藏的商品数量可能超过 100 个),建议使用独立的关联表
  • 使用自定义短 ID 可以增加关联数量
  • 评估业务场景的实际需求,选择合适的方案

多对多中间表操作

对于 MySQL 数据模型的多对多关系,系统会自动创建中间表。虽然不支持直接 SQL JOIN,但可以通过以下方式操作中间表。

查找中间表名称

示例:学生(student)和课程(course)的多对多关系

  • 中间表结构:
    course_student
    ├── course_id (外键)
    ├── student_id (外键)
    └── created_at (创建时间)

查询中间表数据

虽然不支持直接 JOIN,但可以通过数据模型的关联查询获取关联数据:

// 查询学生及其选修的所有课程
const { data } = await models.student.get({
filter: {
where: {
_id: { $eq: "student_123" }
}
},
select: {
_id: true,
name: true,
courses: {
_id: true,
courseName: true,
credits: true
}
}
});

// 结果包含完整的关联数据
console.log(data.courses);
// [
// { _id: "course_1", courseName: "数学", credits: 4 },
// { _id: "course_2", courseName: "英语", credits: 3 }
// ]

反向查询

// 查询课程及选修该课程的所有学生
const { data } = await models.course.get({
filter: {
where: {
_id: { $eq: "course_1" }
}
},
select: {
_id: true,
courseName: true,
students: {
_id: true,
name: true,
age: true
}
}
});

console.log(data.students);
// [
// { _id: "student_1", name: "张三", age: 20 },
// { _id: "student_2", name: "李四", age: 21 }
// ]

使用关联条件查询

// 查询选修了"数学"课程的所有学生
const { data } = await models.student.list({
filter: {
relateWhere: {
courses: {
where: {
courseName: { $eq: "数学" }
}
}
}
},
select: {
_id: true,
name: true,
courses: {
courseName: true
}
}
});

故障排查

关联字段更新失败

错误1:格式错误

错误信息Invalid relation field format关联字段格式不正确

原因:未使用 {_id: "xxx"} 格式

错误示例

// ❌ 错误:直接传字符串
await models.student.update({
filter: { where: { _id: { $eq: "student_123" } } },
data: { class: "class_456" } // 错误格式
});

// ❌ 错误:传入完整对象
await models.student.update({
filter: { where: { _id: { $eq: "student_123" } } },
data: {
class: {
_id: "class_456",
name: "一年级1班" // 不需要传入其他字段
}
}
});

正确示例

// ✅ 正确:使用 {_id: "xxx"} 格式
await models.student.update({
filter: { where: { _id: { $eq: "student_123" } } },
data: {
class: {
_id: "class_456"
}
}
});

错误2:数组格式错误

错误信息Expected array of objects with _id field

原因:一对多或多对多关系中,数组格式不正确

错误示例

// ❌ 错误:直接传字符串数组
await models.student.update({
filter: { where: { _id: { $eq: "student_123" } } },
data: {
courses: ["course_1", "course_2"] // 错误格式
}
});

正确示例

// ✅ 正确:数组中每项使用 {_id: "xxx"} 格式
await models.student.update({
filter: { where: { _id: { $eq: "student_123" } } },
data: {
courses: [
{ _id: "course_1" },
{ _id: "course_2" }
]
}
});

错误3:关联数据不存在

错误信息Related record not found关联记录不存在

原因:关联的 _id 在目标模型中不存在

排查步骤

  1. 检查关联的 _id 是否正确
  2. 在目标模型中查询该记录是否存在
  3. 确认 _id 的拼写和格式是否正确

解决方法

// 先验证关联记录是否存在
const classExists = await models.class.get({
filter: { where: { _id: { $eq: "class_456" } } }
});

if (classExists.data) {
// 记录存在,可以安全关联
await models.student.update({
filter: { where: { _id: { $eq: "student_123" } } },
data: { class: { _id: "class_456" } }
});
} else {
console.error("关联的班级不存在");
}

错误4:字段长度超出限制

错误信息Relation field length exceeds limit字段长度超出限制

原因

  • 单个 _id 超过 256 字节
  • 一对多/多对多数组总长度超过 1024 字节

解决方法:参考上文「关联字段限制说明」部分的解决方案

关联查询结果为空

原因1:权限配置问题

关联模型的权限配置独立于主模型,需要分别设置。

排查步骤

  1. 检查主模型的权限配置
  2. 检查关联模型的权限配置
  3. 确认当前用户是否有权限访问关联数据

示例

// 查询文章及评论,但评论权限不足时,评论数据为空
const { data } = await models.post.get({
filter: { where: { _id: { $eq: "post_123" } } },
select: {
title: true,
comments: { // 如果评论模型权限不足,此处返回空数组
content: true
}
}
});

解决方法

  • 在控制台检查关联模型的权限设置
  • 确保当前用户有读取关联模型数据的权限
  • 或在云函数中使用管理员权限进行查询

原因2:关联数据确实不存在

排查步骤

  1. 在控制台直接查看主记录的关联字段值
  2. 检查关联字段是否为空或包含无效的 _id
  3. 在关联模型中查询对应的记录是否存在

解决方法

// 先查询主记录,检查关联字段
const post = await models.post.get({
filter: { where: { _id: { $eq: "post_123" } } },
select: { comments: true }
});

console.log('关联的评论ID:', post.data.comments);
// 检查是否为空数组或包含无效ID

// 验证关联记录是否存在
const commentIds = post.data.comments.map(c => c._id);
const comments = await models.comment.list({
filter: {
where: {
_id: { $in: commentIds }
}
}
});

console.log('实际存在的评论:', comments.data);

原因3:select 字段配置错误

错误示例

// ❌ 错误:未在 select 中包含关联字段
const { data } = await models.post.get({
filter: { where: { _id: { $eq: "post_123" } } },
select: {
title: true,
content: true
// 缺少 comments 字段,不会返回关联数据
}
});

正确示例

// ✅ 正确:在 select 中明确指定关联字段
const { data } = await models.post.get({
filter: { where: { _id: { $eq: "post_123" } } },
select: {
title: true,
content: true,
comments: { // 必须明确指定关联字段
_id: true,
content: true
}
}
});

原因4:多层关联查询不支持

关联查询目前只支持一层关联,多层嵌套关联不会返回数据。

错误示例

// ❌ 错误:多层关联查询不返回数据
const { data } = await models.post.get({
filter: { where: { _id: { $eq: "post_123" } } },
select: {
title: true,
author: { // 第一层关联 ✅
name: true,
profile: { // ❌ 第二层关联:不支持,不会返回数据
address: { // ❌ 第三层关联:不支持
city: true
}
}
}
}
});

// 查询结果:
// {
// title: "文章标题",
// author: {
// name: "张三"
// // profile 字段不会返回
// }
// }

正确示例

// ✅ 正确:只查询一层关联
const { data } = await models.post.get({
filter: { where: { _id: { $eq: "post_123" } } },
select: {
title: true,
author: { // 一层关联 ✅
_id: true,
name: true,
email: true,
bio: true // 普通字段可以正常查询
}
}
});

解决方法:参考上文「关联查询层级限制说明」中的分步查询或数据冗余方案

多对多关系操作异常

问题:无法找到中间表

原因:对于 MySQL 数据模型,中间表由系统自动创建和管理

解决方法

  • 不需要手动创建中间表
  • 使用数据模型提供的关联查询方法
  • 不要尝试直接通过 SQL JOIN 查询中间表

问题:中间表数据不一致

原因:直接修改了中间表数据,导致关联关系不一致

解决方法

  • 始终通过数据模型的更新方法修改关联关系
  • 不要直接操作中间表数据
  • 如果数据不一致,通过数据模型的更新方法重新设置关联关系
// ✅ 正确:通过数据模型更新关联关系
await models.student.update({
filter: { where: { _id: { $eq: "student_123" } } },
data: {
courses: [
{ _id: "course_1" },
{ _id: "course_2" }
]
}
});

// ❌ 错误:不要直接操作中间表
// await db.query("INSERT INTO course_student ..."); // 不推荐