跳到主要内容

子表单场景指南

概述

子表单场景用于处理 一对多 的数据录入需求,例如采购订单中需要录入多个商品明细,每个明细包含商品名称、价格、数量等字段。为了高效管理数据,通常将订单和商品明细分别存储在不同的表中,并通过外键关联绑定。

适用场景
  • 订单与商品明细
  • 客户与联系人
  • 项目与任务清单
  • 其他一对多的数据关系

数据关联设计

在子表单场景中,主子表通过「外键」方式进行关联:

关联原则

  • 子表存储外键:在子表中新建外键字段(如 order_id),指向主表记录的 _id
  • 主表无感知:主表不需要维护子表关系,保持数据结构简洁
  • 单向关联:子表 → 主表的单向引用,避免双向维护的复杂性

关联配置示例

子表新建 orderId 字段关联主表 order 表的 _id 字段,实现主子表关联。

删除规则

新建外键时,删除规则可以选择为「级联」,当删除被引用表(order 主表)中的数据时,同时删除当前表(商品明细表)中引用该数据的记录。

数据处理流程

在页面提交时,数据处理顺序如下:

  1. 先处理主表:新增或更新主表数据,获取主表记录的 _id
  2. 再处理子表:使用主表 _id 作为外键,批量处理子表的增删改操作
注意

如果使用了关联关系字段,需要在云函数中单独处理字段格式转换(将简单值转换为 {_id: value} 格式)。

由于涉及多次数据库交互和数据对比逻辑,子表数据处理将使用「云函数」来实现,前端只需调用云函数即可完成数据更新。

子表单云函数实现

处理步骤

子表单的处理分为以下步骤:

1. 数据拆分

云函数接收子表单数据并通过对比「当前数据」与「原始数据」,分别获取需要增删改的数据:

  • 新增数据:当前数据中不存在 _id 的记录
  • 更新数据:当前数据中存在 _id,且字段值与原始数据不同的记录
  • 删除数据:原始数据中存在,但当前数据中不存在的记录

2. 批量操作

  • 批量新增:收集所有新增数据,使用 createMany 一次性创建
  • 并行更新:收集所有更新数据,使用 Promise.all 并行执行多个更新操作
  • 解除关联:将子表单数据外键字段设为 null 即可,若需要取消关联时就删除数据则也可在该环节实现

3. 关联字段处理

因关联关系字段在新增时数据格式为 {_id: value},因此需要单独处理转换。

云函数参数说明

云函数的入参格式如下:

参数类型说明
mainIdstring主表数据对象 _id
subListSubTableItem[]子表列表,包含所有需要处理的子表信息
isProdboolean环境标识,true 为生产环境,false 为预发布环境

SubTableItem 格式如下:

参数类型说明
subNamestring子表的数据模型名称
parentIdstring子表中关联主表的外键字段
subDataObject[]子表当前数据数组
subOldDataObject[]子表原始数据数组(用于对比)
relateFieldstring关联关系字段 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 方法是子表数据处理的核心,主要逻辑如下:

  1. 数据对比:遍历 subData,通过 _id 是否存在判断是新增还是更新
  2. 字段对比:对于更新操作,逐字段对比新旧数据,只更新变化的字段
  3. 批量操作:使用 createMany 批量新增,使用 Promise.all 并行更新
  4. 删除处理:通过对比新旧数据的 _id 列表,找出需要删除的记录,将其外键设为 null
  5. 关联转换:通过 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

  1. 配置数据表:选择 采购商品明细(order_items)
  2. 触发方式:选择 手动触发执行
  3. 查询条件:设置 所属订单 等于 $w.page.dataset.params._id(从 URL 传过来的主表 _id

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

步骤 2:配置商品列表组件

在表单中添加「数组嵌套表单」组件作为商品明细列表:

  1. 修改 嵌套表单模板对象数组(表格)
  2. 添加子表字段:
    • 商品名称(name)- 文本字段
    • 采购数量(quantity)- 数字输入
    • 商品价格(price)- 数字输入
  3. 设置数组嵌套表单的值,改为从 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.id
  • subList:子表配置数组
    • 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()