子表单场景指南
概述
子表单场景用于处理 一对多 的数据录入需求,例如采购订单中需要录入多个商品明细,每个明细包含商品名称、价格、数量等字段。为了高效管理数据,通常将订单和商品明细分别存储在不同的表中,并通过外键关联绑定。
- 订单与商品明细
- 客户与联系人
- 项目与任务清单
- 其他一对多的数据关系
数据关联设计
在子表单场景中,主子表通过「外键」方式进行关联:
关联原则
- 子表存储外键:在子表中新建外键字段(如
order_id),指向主表记录的_id - 主表无感知:主表不需要维护子表关系,保持数据结构简洁
- 单向关联:子表 → 主表的单向引用,避免双向维护的复杂性
关联配置示例
子表新建 orderId 字段关联主表 order 表的 _id 字段,实现主子表关联。

新建外键时,删除规则可以选择为「级联」,当删除被引用表(order 主表)中的数据时,同时删除当前表(商品明细表)中引用该数据的记录。
数据处理流程
在页面提交时,数据处理顺序如下:
- 先处理主表:新增或更新主表数据,获取主表记录的
_id - 再处理子表:使用主表
_id作为外键,批量处理子表的增删改操作
如果使用了关联关系字段,需要在云函数中单独处理字段格式转换(将简单值转换为 {_id: value} 格式)。
由于涉及多次数据库交互和数据对比逻辑,子表数据处理将使用「云函数」来实现,前端只需调用云函数即可完成数据更新。
子表单云函数实现
处理步骤
子表单的处理分为以下步骤:
1. 数据拆分
云函数接收子表单数据并通过对比「当前数据」与「原始数据」,分别获取需要增删改的数据:
- 新增数据:当前数据中不存在
_id的记录 - 更新数据:当前数据中存在
_id,且字段值与原始数据不同的记录 - 删除数据:原始数据中存在,但当前数据中不存在的记录
2. 批量操作
- 批量新增:收集所有新增数据,使用
createMany一次性创建 - 并行更新:收集所有更新数据,使用
Promise.all并行执行多个更新操作 - 解除关联:将子表单数据外键字段设为
null即可,若需要取消关联时就删除数据则也可在该环节实现
3. 关联字段处理
因关联关系字段在新增时数据格式为 {_id: value},因此需要单独处理转换。
云函数参数说明
云函数的入参格式如下:
| 参数 | 类型 | 说明 |
|---|---|---|
| mainId | string | 主表数据对象 _id |
| subList | SubTableItem[] | 子表列表,包含所有需要处理的子表信息 |
| isProd | boolean | 环境标识,true 为生产环境,false 为预发布环境 |
SubTableItem 格式如下:
| 参数 | 类型 | 说明 |
|---|---|---|
| subName | string | 子表的数据模型名称 |
| parentId | string | 子表中关联主表的外键字段 |
| subData | Object[] | 子表当前数据数组 |
| subOldData | Object[] | 子表原始数据数组(用于对比) |
| relateField | string | 关联关系字段 key,多个用逗号分隔 |
完整代码示例
查看完整云函数代码
const cloudbase = require('@cloudbase/node-sdk');
const app = cloudbase.init({
env: cloudbase.SYMBOL_CURRENT_ENV,
});
const models = app.models;
/**
* 处理主表与多个子表数据关系
* @param {Object} params - 函数参数对象
* @param {string} params.mainId - 主表数据对象 _id
* @param {SubTableItem[]} params.subList - 子表列表,包含所有需要处理的子表信息
* @param {boolean} [params.isProd=false] - 环境标识,true 为生产环境,false 为预发布环境
* @returns {Promise<any>} 返回处理结果对象
*/
exports.main = async function ({ mainId, subList, isProd = false }, context) {
try {
const envType = isProd ? 'prod' : 'pre';
// 处理每个子表的增删改操作
for (const subItem of subList) {
const { subName } = subItem;
console.log(`开始更新子表 ${subName} 数据`);
await executeSubTableOperations(subItem, mainId, envType);
console.log(`更新子表 ${subName} 完毕`);
}
return {
success: true,
message: '数据处理成功',
};
} catch (error) {
console.error('数据处理失败:', error);
throw new Error(`数据处理失败: ${error.message}`);
}
};
/**
* 将关联字段转换为对象格式
* @param {string} relateField - 关联字段名,多个用逗号分隔
* @param {Object} obj - 原始数据对象
* @returns {Object} 转换后的数据对象
*/
function transRelateField(relateField, obj) {
if (!relateField) return obj;
const fields = relateField.split(',');
return fields.reduce((acc, field) => {
if (acc[field]) {
acc[field] = {
_id: acc[field],
};
}
return acc;
}, { ...obj });
}
/**
* 执行子表数据的增删改操作
* @param {SubTableItem} subItem - 子表信息
* @param {string} mainId - 主表数据对象 _id
* @param {string} envType - 环境类型
* @returns {Promise<void>}
*/
async function executeSubTableOperations(subItem, mainId, envType) {
const { subName, parentId, subData, subOldData, relateField } = subItem;
const model = models[subName];
// 参数验证
if (!model) {
throw new Error(`数据模型 ${subName} 不存在`);
}
if (!Array.isArray(subData)) {
throw new Error(`子表数据 ${subName} 必须是数组格式`);
}
// 收集需要新增和更新的数据
const addDataList = [];
const updateList = [];
const updatePromises = [];
// 1. 遍历当前数据,处理新增和更新
for (const item of subData) {
if (!item._id) {
// 新增数据
addDataList.push(item);
} else {
// 检查是否需要更新(对比旧数据)
const oldItem = subOldData?.find((old) => old._id === item._id);
if (oldItem) {
const updateFields = {};
// 对比所有字段,记录变化的部分
Object.keys(item).forEach((key) => {
if (key !== '_id' && item[key] !== oldItem[key]) {
updateFields[key] = item[key];
}
});
// 如果有字段变化,执行更新
if (Object.keys(updateFields).length > 0) {
const updateData = transRelateField(relateField, {
[parentId]: mainId,
...updateFields,
});
const updatePromise = model
.update({
envType,
data: updateData,
filter: {
where: {
_id: {
$eq: item._id,
},
},
},
})
.then((res) => ({
type: 'edit',
data: res,
_id: item._id,
}));
updateList.push(updateData);
updatePromises.push(updatePromise);
}
}
}
}
// 2. 批量新增数据
let addPromises = null;
let dataWithParent = null;
if (addDataList.length > 0) {
// 为新增数据添加主表关联
dataWithParent = addDataList.map((item) =>
transRelateField(relateField, { ...item, [parentId]: mainId })
);
addPromises = model.createMany({
envType,
data: dataWithParent,
});
}
// 3. 处理删除操作(解除关联)
const newIds = subData.filter((item) => item._id).map((item) => item._id);
const oldIds = subOldData?.map((item) => item._id) || [];
const deleteIds = oldIds.filter((id) => !newIds.includes(id));
let deletePromises = null;
if (deleteIds.length > 0) {
// 将外键设为 null,解除关联
deletePromises = model.updateMany({
envType,
data: { [parentId]: null },
filter: {
where: {
_id: {
$in: deleteIds,
},
},
},
});
}
// 日志输出
console.log(`${subName} 子表操作明细:`);
console.log('- 新增数据:', dataWithParent);
console.log('- 更新数据:', updateList);
console.log('- 解除关联:', deleteIds);
// 执行所有数据库操作
const allPromises = [];
if (addPromises) allPromises.push(addPromises);
if (deletePromises) allPromises.push(deletePromises);
allPromises.push(...updatePromises);
if (allPromises.length > 0) {
await Promise.all(allPromises);
}
}
/**
* @typedef {Object} SubTableItem - 子表信息对象
* @property {string} subName - 子表的数据模型名称
* @property {string} parentId - 子表中关联主表的 ID
* @property {Object[]} subData - 子表当前数据数组
* @property {Object[]} subOldData - 子表原始数据数组(用于对比)
* @property {string} relateField - 关联关系字段 key,多个用逗号分隔
*/
核心逻辑说明
executeSubTableOperations 方法是子表数据处理的核心,主要逻辑如下:
- 数据对比:遍历
subData,通过_id是否存在判断是新增还是更新 - 字段对比:对于更新操作,逐字段对比新旧数据,只更新变化的字段
- 批量操作:使用
createMany批量新增,使用Promise.all并行更新 - 删除处理:通过对比新旧数据的
_id列表,找出需要删除的记录,将其外键设为null - 关联转换:通过
transRelateField函数自动将指定字段转换为关联对象格式{_id: value}
- 使用
createMany批量创建,减少数据库交互次数 - 使用
Promise.all并行执行更新操作,提升性能 - 只更新变化的字段,减少不必要的数据库写入
示例场景开发
这里以「采购管理」系统为例,演示如何使用子表功能。
数据关系如下:

新建数据模型
进入 MySQL 数据库/数据模型 新建如下两个数据模型:
采购订单(purchase_order)
| 字段名 | 类型 | 描述 |
|---|---|---|
| name | 文本 | 订单名称 |
| amount | 数字 | 总金额 |
| status | 枚举 | 订单状态 |
主表不需要维护子表关系字段。
采购商品明细(order_items)
| 字段名 | 类型 | 描述 |
|---|---|---|
| name | 文本 | 商品名称 |
| price | 数字 | 商品价格 |
| quantity | 数字 | 采购数量 |
| order_id | 文本 | 所属订单(外键) |
order_id 字段为外键,指向主表 purchase_order 的 _id。
该外键关系需要在创建完数据模型后,去 MySQL数据库/数据库表 对应的表中进行创建
新增应用
新建一个空白应用,通过模板快速生成「表格与表单页」,数据模型选择 采购订单(purchase_order)。

配置编辑页
步骤 1:获取子表数据
进入编辑页面,由于表单容器绑定的是 采购订单 数据源,因此无法直接获取子表数据,需要手动调用「内置数据表查询」方法来获取子表数据。
在当前页面新增一个「内置数据表查询」方法,命名为 getOrderDetail:
- 配置数据表:选择 采购商品明细(order_items)
- 触发方式:选择 手动触发执行
- 查询条件:设置 所属订单 等于
$w.page.dataset.params._id(从 URL 传过来的主表_id)

- 在编辑页面「表单容器」的 查询成功事件 中,添加执行
getOrderDetail方法。这样主表表单数据加载完成后就会自动加载子表数据。

步骤 2:配置商品列表组件
在表单中添加「数组嵌套表单」组件作为商品明细列表:
- 修改 嵌套表单模板 为 对象数组(表格)
- 添加子表字段:
- 商品名称(name)- 文本字段
- 采购数量(quantity)- 数字输入
- 商品价格(price)- 数字输入
- 设置数组嵌套表单的值,改为从
getOrderDetail中获取:
$w.getOrderDetail.data?.records?.length ? $w.getOrderDetail.data?.records : [{}]

步骤 3:配置提交事件
修改「表单容器」的 提交事件,在主表更新数据成功时调用云函数处理子表数据:

调用云函数代码如下:
() => {
$w.cloud.callFunction({
name: "subTable-management", // 云函数名称
data: {
mainId: $w.page.dataset.params._id || event.detail.id,
subList: [{
subName: 'order_items',
parentId: 'orderId',
subData: $w.formArr2.value,
subOldData: $w.getOrderDetail.data.records,
relateField: 'gongyingshang'
}]
},
})
}
参数说明:
mainId:主表_id。若为编辑态,使用$w.page.dataset.params._id;若为新增状态,从上一环节「调用数据源方法」的返回值中获取event.detail.idsubList:子表配置数组subName:子表数据模型名称parentId:子表中关联主表的字段名称subData:子表当前数据数组(formArr2为子表组件 ID,通过$w.formArr2.value获取)subOldData:子表原始数据数组(用于对比)relateField:关联关系字段 key(可选,多个用逗号分隔)
进阶用法
多个子表处理
如果一个主表关联多个子表,可以在 subList 中添加多个子表配置:
await $w.cloud.callFunction({
name: 'subTable-management',
data: {
mainId: mainId,
subList: [
{
subName: 'order_items',
parentId: 'order_id',
subData: $w.form1.value?.order_items || [],
subOldData: $w.form1.remoteValue?.order_items || [],
},
{
subName: 'order_attachments', // 第二个子表
parentId: 'order_id',
subData: $w.form1.value?.attachments || [],
subOldData: $w.form1.remoteValue?.attachments || [],
}
],
isProd: false
},
});
关联字段转换
如果子表中有其他关联字段(如供应商、分类等),需要使用 relateField 参数:
{
subName: 'order_items',
parentId: 'order_id',
subData: $w.form1.value?.order_items || [],
subOldData: $w.form1.remoteValue?.order_items || [],
relateField: 'supplier,category' // 需要转换的字段,多个用逗号分隔
}
云函数会自动将这些字段从简单值(如 "supplier_id_value")转换为关联对象格式 {_id: "supplier_id_value"}。
完整示例模版
完整示例模版 点击获取,下载后前往 云开发平台/微搭低代码/自建模版管理 中进行导入模版,即可使用。
常见问题
1. 为什么主表不需要维护子表关系?
使用外键的单向关联设计,子表通过外键指向主表,主表无需维护子表信息。这样可以:
- 简化主表数据结构
- 避免数据冗余
- 降低数据维护复杂度
2. 删除操作是物理删除还是逻辑删除?
本方案采用「解除关联」的方式,将子表的外键字段设为 null,而不是物理删除记录。如果需要物理删除,可以在云函数的删除处理部分修改为 model.modelName.delete()。