当前位置: 首页 > news >正文

一对多的数据库姻缘:ArkTS 为鸿蒙订单设计外键与明细表


实例:订单与明细(Order)|技术:订单表 + 明细表、外键约束、OrderDao

一、业务需求分析:订单为什么需要两张表

「一个订单包含多件商品」是最经典的一对多关系:一张订单(SO20250601001)包含蓝牙耳机、数据线、手机壳三件商品。如果把商品直接塞进订单表(逗号分隔),查询「这件商品卖了多少单」会变成噩梦;正确的建模是拆成两张表

  • 订单表 orders:订单本体(订单号、状态、总价、收货人、时间)——一单一行;
  • 明细表 order_item:订单里的每件商品(商品名、单价、数量、小计)——一商品一行,用 order_id 指向所属订单。

这个「一(订单)对多(明细)」的结构,是电商、点餐、进销存(实例 6 已见雏形)的通用数据模型,也是 JOIN 联表查询的天然舞台。

核心业务需求:

  1. 下单:一次提交「订单信息 + 多件商品明细」,事务保证原子(全成或全撤);
  2. 订单列表:每个订单卡片内嵌其明细(主从嵌套列表);
  3. 状态流转:待支付 → 已支付 → 已发货 → 已完成(或取消);
  4. 总价计算:明细小计之和 = 订单总价(事务内计算);
  5. 状态统计:每种状态的订单数(GROUP BY)。

二、双表字段设计

订单表 orders

字段名类型约束说明
idINTEGERPRIMARY KEY AUTOINCREMENT自增主键
order_noTEXTNOT NULL UNIQUE订单号(SO+时间戳)
statusINTEGERNOT NULL DEFAULT 00 待支付 / 1 已支付 / 2 已发货 / 3 已完成 / 4 已取消
total_amountREALNOT NULL DEFAULT 0订单总价
customerTEXTNOT NULL收货人
phoneTEXTDEFAULT ‘’联系电话
addressTEXTDEFAULT ‘’收货地址
created_timeINTEGERNOT NULL下单时间戳

明细表 order_item

字段名类型约束说明
idINTEGERPRIMARY KEY AUTOINCREMENT自增主键
order_idINTEGERNOT NULL所属订单(逻辑外键)
product_nameTEXTNOT NULL商品名(快照)
priceREALNOT NULL成交单价
quantityINTEGERNOT NULL数量
subtotalREALNOT NULL小计(price × quantity)

设计要点拆解

1. order_no 业务唯一键UNIQUE约束保证订单号唯一——业务上订单号是「对外标识」(客服查单、物流对账都用它),比自增 id 更有业务意义。'SO' + Date.now()生成规则简单可靠(毫秒级不会撞号)。

2. status 用 0~4 数字枚举。五个状态:待支付 → 已支付 → 已发货 → 已完成 → 已取消。数字编码可参与WHERE status=0过滤和状态统计,页面映射成文案。

3. product_name 存快照而非外键。明细里的商品名直接存文本(不引用商品表)——这是「快照」设计:订单确定后商品名/价格就是历史事实,不能随商品表变化。如果用户买了「无线蓝牙耳机」后商家改名「蓝牙耳机 Pro」,订单明细应保持当时的名称。价格同理(price快照成交价)。

4. total_amount 冗余在订单表。总价 = Σ(明细小计),但订单列表高频读取总价(卡片、统计、导出),冗余存储避免每次都 JOIN + SUM。冗余的代价是写入时必须保证一致——下单事务里先算 total 再写入(见 9-3 文章)。

5. 逻辑外键 order_id。与实例 6 相同,用order_id INTEGER NOT NULL+ 代码级联删除(deleteOrder 先删明细再删订单),不启用物理 FOREIGN KEY。

三、建表 SQL:双表 + 明细索引

CREATETABLEIFNOTEXISTSorders(idINTEGERPRIMARYKEYAUTOINCREMENT,order_noTEXTNOTNULLUNIQUE,statusINTEGERNOTNULLDEFAULT0,total_amountREALNOTNULLDEFAULT0,customerTEXTNOTNULL,phoneTEXTDEFAULT'',addressTEXTDEFAULT'',created_timeINTEGERNOTNULL);CREATETABLEIFNOTEXISTSorder_item(idINTEGERPRIMARYKEYAUTOINCREMENT,order_idINTEGERNOTNULL,product_nameTEXTNOTNULL,priceREALNOTNULL,quantityINTEGERNOTNULL,subtotalREALNOTNULL);CREATEINDEXIFNOTEXISTSidx_item_orderONorder_item(order_id);

idx_item_order索引的意义:明细表的高频查询是「某订单的全部明细」(WHERE order_id = ?)——没有索引,每次全表扫描;有索引,直接定位。外键列必须建索引,这是一对多建模的铁律。

表名orders而非orderorder是 SQL 的保留关键字(ORDER BY),用orders复数避开歧义——这是表命名的一个实用技巧。

四、OrderDao 封装:实体与聚合体

数据层核心OrderDao,实体接口:

exportinterfaceOrder{id:number;orderNo:string;status:number;totalAmount:number;customer:string;phone:string;address:string;createdTime:number;}exportinterfaceOrderItem{id:number;orderId:number;productName:string;price:number;quantity:number;subtotal:number;}/** 订单 + 明细聚合体(页面渲染用) */exportinterfaceOrderWithItems{order:Order;items:OrderItem[];}

OrderWithItems聚合体接口是本实例的亮点——它不是数据库表对应的实体,而是**「查询结果导向」的组合体**:一个订单 + 它的明细数组。页面ForEach(this.orders)直接渲染聚合体,数据层负责组装。聚合体接口让 DAO 返回「页面想要的形状」,UI 零组装。

下单参数用OrderDraft(订单草稿)+OrderItemDraft(明细草稿)表达「待创建的数据」:

exportinterfaceOrderDraft{customer:string;phone:string;address:string;items:OrderItemDraft[];}exportinterfaceOrderItemDraft{productName:string;price:number;quantity:number;}

为什么分开 Draft 和实体?草稿没有 id、orderId、subtotal(下单时才计算),语义上区分「输入数据」与「存储数据」——这是分层架构的细节修养。

五、基础查询:订单列表与单订单明细

全部订单(时间倒序)

staticasyncqueryOrders(context:common.Context):Promise<Order[]>{conststore=awaitOrderDao.getStore(context);constpredicates=newrelationalStore.RdbPredicates(OrderDao.TABLE);predicates.orderByDesc('created_time');constresult=awaitstore.query(predicates);returnOrderDao.collectOrders(result);}

某订单的全部明细

staticasyncqueryItemsByOrder(context:common.Context,orderId:number):Promise<OrderItem[]>{conststore=awaitOrderDao.getStore(context);constpredicates=newrelationalStore.RdbPredicates(OrderDao.ITEM_TABLE);predicates.equalTo('order_id',orderId).orderByAsc('id');constresult=awaitstore.query(predicates);returnOrderDao.collectItems(result);}

这两个基础查询是一对多关系的「按需加载」形态——先查订单列表,再按需查每个订单的明细。9-3 文章会介绍更高效的「一次 JOIN 查出全部」形态。

六、技术要点对照表

技术点实现方式生产价值
一对多orders + order_item 双表订单与商品解耦
外键索引idx_item_order明细查询走索引
业务唯一键order_no UNIQUE对外标识唯一
快照product_name/price 存当时值历史事实不变
冗余总价total_amount列表高频读 O(1)
聚合体OrderWithItems 接口页面零组装
枚举状态status 0~4可过滤可统计

七、文章小结

订单实例建立了一对多双表模型:orders 存订单本体,order_item 存明细,order_id 关联 + 索引加速;明细用「快照」保存商品名和价格(历史事实不随主数据变),总价冗余在订单表(高频读 O(1))。OrderWithItems聚合体接口让 DAO 直接返回「页面想要的形状」。这是电商/点餐/进销存共同的骨架。

下一篇(9-2)展示主从嵌套列表 UI——订单卡片内嵌明细,状态标签 + 总价一目了然。


八、双表设计再深挖:主外键关联、状态枚举与金额精度

8.1 主外键关联:逻辑外键与物理外键的取舍

orders 与 order_item 通过order_id关联。SQLite 默认不强制外键(PRAGMA foreign_keys默认 OFF),所以本实例采用逻辑外键order_id INTEGER NOT NULL+ 应用层保证一致性(删除订单先删明细)。为什么不用物理 FOREIGN KEY?

方案优点缺点本实例选择
物理外键 FOREIGN KEY … REFERENCES orders(id)数据库强制引用完整性需开 PRAGMA、SQLite 支持弱、级联行为繁琐
逻辑外键(代码关联)简单直观、删除可控、无 PRAGMA 依赖应用层要自己保证一致

relationalStore 的 delete 支持 predicates,「先删明细再删订单」两行代码即可闭环:

// 删除订单:先删明细,再删订单(代码级联)constitemPredicates=newrelationalStore.RdbPredicates(OrderDao.ITEM_TABLE);itemPredicates.equalTo('order_id',orderId);awaitstore.delete(itemPredicates);constorderPredicates=newrelationalStore.RdbPredicates(OrderDao.TABLE);orderPredicates.equalTo('id',orderId);awaitstore.delete(orderPredicates);

8.2 订单状态枚举:0~4 数字编码的工程意义

status 用数字枚举而非文本,三个理由:

  1. 可过滤可统计WHERE status = 1GROUP BY status直接数值运算;
  2. 存储紧凑:INTEGER 4 字节 vs TEXT 变长;
  3. 可排序:状态流转 0→1→2→3 天然有序(数字序 = 流程序)。

页面侧用映射函数把数字翻译成文案 + 标签色:

status文案标签色
0待支付橙色
1已支付蓝色
2已发货紫色
3已完成绿色
4已取消灰色
exportfunctionstatusText(status:number):string{constmap:string[]=['待支付','已支付','已发货','已完成','已取消'];returnmap[status]??'未知';}

8.3 金额为什么用 REAL:SQLite 没有 DECIMAL

SQLite 只有 INTEGER/REAL/TEXT/BLOB 四种存储类,没有数据库界的 DECIMAL(10,2)。金额用 REAL 后,0.1 + 0.2这类浮点误差真实存在(0.30000000000000004)。本实例的应对策略:下单计算用Math.round(total * 100) / 100保留两位小数(见 9-3),页面展示用toFixed(2)格式化(见 9-2):

// 金额规整:先乘 100 取整再除 100,消除浮点尾差consttotal=Math.round(sum*100)/100;

九、为什么主从表:一单多品的范式设计

如果只建一张表「订单+商品平铺」,每件商品一行就会重复订单头信息(收货人、地址、电话各重复 N 遍),产生三类问题:

  1. 冗余:同一订单的收货人存 N 份,改地址要改 N 行;
  2. 更新异常:漏改一行就数据不一致;
  3. 语义错乱:「一笔订单」和「一件商品」混在一张表,订单状态、总价无从归属。

主从表(一对多)设计让 orders 每行代表「一个订单实体」,order_item 每行代表「一件商品」,用外键连接——这就是**第三范式(3NF)**的落地:非主属性完全依赖主键,明细只依赖 order_id,不重复订单头信息。

范式维度单表平铺主从双表
订单头冗余N 件商品重复 N 次0 冗余
改收货人改 N 行改 1 行
查询某订单明细全表过滤order_id 索引直达
删除订单逐行删先明细后订单,两行代码

十、JOIN 联表查询的思路预览

9-1 的「按需加载」先查订单再查明细,N 个订单要 N+1 次查询;JOIN 一次查出全部:

SELECTo.*,i.product_name,i.price,i.quantity,i.subtotalFROMorders oLEFTJOINorder_item iONo.id=i.order_idORDERBYo.created_timeDESC,o.idDESC;

JOIN 的两种形态在 9-3 深入:INNER JOIN(只出有明细的订单)与LEFT JOIN(保留无明细的订单)。配合idx_item_order,JOIN 的关联条件o.id = i.order_id走索引而非全表扫描——索引是 JOIN 性能的前提,这就是为什么外键列必须建索引。

十一、索引设计复盘:订单号与外键

索引所在表字段服务的高频查询
主键索引(自动)ordersidWHERE id = ?单订单查询
UNIQUE 隐式索引(自动)ordersorder_noWHERE order_no = ?客服查单
idx_item_order(手动)order_itemorder_idWHERE order_id = ?明细查询 / JOIN 关联

SQLite 中PRIMARY KEYUNIQUE会自动建索引,idx_item_order是唯一需要手动建的。三把索引覆盖了订单模块的全部高频查询路径——建表时想清楚「哪些列会被 WHERE/JOIN 命中」,索引设计就完成了。

十二、FAQ

Q1:为什么不用物理外键?
SQLite 的外键需要每次连接PRAGMA foreign_keys = ON,relationalStore 封装下开启繁琐;且级联删除的默认行为(RESTRICT)反而要写额外代码。逻辑外键 + 应用层两行级联更直白。

Q2:status 为什么不用字符串 ‘PAID’ 而用数字?
数字可参与GROUP BY status统计和区间比较,字符串只能等值匹配;且数字编码天然对应流程顺序。代价是阅读性差,用statusText()映射函数弥补。

Q3:金额 REAL 会不会出错?
有理论误差,但两位小数的场景下Math.round(x * 100) / 100规整后展示无感。生产环境金额敏感系统可考虑「以分为单位存 INTEGER」——本实例为教学清晰选 REAL。

Q4:order_no 用 Date.now() 会不会撞号?
毫秒级时间戳在单机单线程下不会撞;多设备离线场景可加设备前缀(如SO${deviceId}${Date.now()}),UNIQUE 约束兜底防重。

Q5:为什么明细表存 product_name 快照而不存商品 id?
订单是历史事实。若引用商品表,商家改价/改名后历史订单会被「篡改」,财务对账就出问题。快照 + 成交价才是订单明细的正解。

Q6:查询明细时orderByAsc('id')的意义?
明细按插入顺序(id 自增)返回,保证商品顺序与下单时一致——UI 展示的明细顺序就是用户加购的顺序。

http://www.jsqmd.com/news/1401905/

相关文章:

  • 粗排与精排:揭秘大规模推荐系统的核心排序架构
  • AI Agent开发实战:从大模型到智能体的技术跃迁与应用
  • SystemVerilog中‘1‘与‘b1‘赋值差异详解:填充规则与位宽陷阱
  • 01_合同效力与招投标瑕疵
  • 什么是TG?极简介绍与核心应用
  • 2026居家轻量化直播实测 普通人副业低压力稳定增收指南 - nuanyin
  • 批量重命名命令失效全解析:从诊断到安全执行的完整指南
  • 关于画图工具的优化
  • Lua环境搭建全攻略:从零配置独立开发环境到包管理
  • 双列瀑布流之美:ArkUI 触底加载动画让鸿蒙商品页刷到手软
  • 知网AIGC检测前,先用哪个免费检测自查
  • 2026 年至今,广东专业的化工设备拆除回收服务商找哪家,你家闲置的旧厂“大块头”,藏着不为人知的变现窍门? - 行业推荐官【认证】
  • 美团“红灯停表”悄悄按下了骑手计时器的暂停键
  • 2026年试试这几家车间精益安灯系统整体解决方案提供商,让管理效率翻倍 - 品牌排行榜
  • 小米8E5解锁工具更新啦,又叫8 Elite Gen5解锁脚本
  • 数据的身家性命:ArkTS 为鸿蒙备份导出设计目标库结构
  • 桌面快捷方式图标变白最保险解决方式
  • Eclipse环境下的PVZ模组开发:实现昼夜交替与日食机制
  • 温度与湿度双重作用下的肌肤护理策略
  • 懒加载的秘密:ArkTS 实现鸿蒙商品分页与总条数统计
  • OpenClaw新手必备:10个高效技能包快速搭建AI助手
  • 论文尾巴降AI按字怎么弄,助研君最省心
  • Prism框架区域与导航机制:构建模块化WPF/Xamarin.Forms应用的核心
  • Windows安全中心页面不可用?从服务检查到注册表修复的完整解决方案
  • 2026 年新消息:浦北口碑好的公路防撞护栏生产厂家哪家靠谱,被忽略的公路保命装置,居然还有这么多不为人知的细节?-煜翎丝网 - 行业推荐官[官方】--
  • 工业软件授权分发技术解析与实践
  • 黑客攻防实战:从渗透测试到APT攻击防御
  • Mac 上跑通 minazapper 狗叫分类器:从“假 100%“到真实可用的踩坑指南
  • 构建多智能体协作系统:从协议设计到工程实践
  • 【C++】CSP-J复赛模拟赛2