FEATURED · 精选文章

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

发布时间 / 2026/8/16 2:22:16
来源 / 创域科博编辑部
栏目 / 资讯中心
一对多的数据库姻缘:ArkTS 为鸿蒙订单设计外键与明细表 实例订单与明细Order技术订单表 明细表、外键约束、OrderDao一、业务需求分析订单为什么需要两张表「一个订单包含多件商品」是最经典的一对多关系一张订单SO20250601001包含蓝牙耳机、数据线、手机壳三件商品。如果把商品直接塞进订单表逗号分隔查询「这件商品卖了多少单」会变成噩梦正确的建模是拆成两张表订单表 orders订单本体订单号、状态、总价、收货人、时间——一单一行明细表 order_item订单里的每件商品商品名、单价、数量、小计——一商品一行用 order_id 指向所属订单。这个「一订单对多明细」的结构是电商、点餐、进销存实例 6 已见雏形的通用数据模型也是 JOIN 联表查询的天然舞台。核心业务需求下单一次提交「订单信息 多件商品明细」事务保证原子全成或全撤订单列表每个订单卡片内嵌其明细主从嵌套列表状态流转待支付 → 已支付 → 已发货 → 已完成或取消总价计算明细小计之和 订单总价事务内计算状态统计每种状态的订单数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 status0过滤和状态统计页面映射成文案。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):PromiseOrder[]{conststoreawaitOrderDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(OrderDao.TABLE);predicates.orderByDesc(created_time);constresultawaitstore.query(predicates);returnOrderDao.collectOrders(result);}某订单的全部明细staticasyncqueryItemsByOrder(context:common.Context,orderId:number):PromiseOrderItem[]{conststoreawaitOrderDao.getStore(context);constpredicatesnewrelationalStore.RdbPredicates(OrderDao.ITEM_TABLE);predicates.equalTo(order_id,orderId).orderByAsc(id);constresultawaitstore.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「先删明细再删订单」两行代码即可闭环// 删除订单先删明细再删订单代码级联constitemPredicatesnewrelationalStore.RdbPredicates(OrderDao.ITEM_TABLE);itemPredicates.equalTo(order_id,orderId);awaitstore.delete(itemPredicates);constorderPredicatesnewrelationalStore.RdbPredicates(OrderDao.TABLE);orderPredicates.equalTo(id,orderId);awaitstore.delete(orderPredicates);8.2 订单状态枚举0~4 数字编码的工程意义status 用数字枚举而非文本三个理由可过滤可统计WHERE status 1、GROUP BY status直接数值运算存储紧凑INTEGER 4 字节 vs TEXT 变长可排序状态流转 0→1→2→3 天然有序数字序 流程序。页面侧用映射函数把数字翻译成文案 标签色status文案标签色0待支付橙色1已支付蓝色2已发货紫色3已完成绿色4已取消灰色exportfunctionstatusText(status:number):string{constmap:string[][待支付,已支付,已发货,已完成,已取消];returnmap[status]??未知;}8.3 金额为什么用 REALSQLite 没有 DECIMALSQLite 只有 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消除浮点尾差consttotalMath.round(sum*100)/100;九、为什么主从表一单多品的范式设计如果只建一张表「订单商品平铺」每件商品一行就会重复订单头信息收货人、地址、电话各重复 N 遍产生三类问题冗余同一订单的收货人存 N 份改地址要改 N 行更新异常漏改一行就数据不一致语义错乱「一笔订单」和「一件商品」混在一张表订单状态、总价无从归属。主从表一对多设计让 orders 每行代表「一个订单实体」order_item 每行代表「一件商品」用外键连接——这就是**第三范式3NF**的落地非主属性完全依赖主键明细只依赖 order_id不重复订单头信息。范式维度单表平铺主从双表订单头冗余N 件商品重复 N 次0 冗余改收货人改 N 行改 1 行查询某订单明细全表过滤order_id 索引直达删除订单逐行删先明细后订单两行代码十、JOIN 联表查询的思路预览9-1 的「按需加载」先查订单再查明细N 个订单要 N1 次查询JOIN 一次查出全部SELECTo.*,i.product_name,i.price,i.quantity,i.subtotalFROMorders oLEFTJOINorder_item iONo.idi.order_idORDERBYo.created_timeDESC,o.idDESC;JOIN 的两种形态在 9-3 深入INNER JOIN只出有明细的订单与LEFT JOIN保留无明细的订单。配合idx_item_orderJOIN 的关联条件o.id i.order_id走索引而非全表扫描——索引是 JOIN 性能的前提这就是为什么外键列必须建索引。十一、索引设计复盘订单号与外键索引所在表字段服务的高频查询主键索引自动ordersidWHERE id ?单订单查询UNIQUE 隐式索引自动ordersorder_noWHERE order_no ?客服查单idx_item_order手动order_itemorder_idWHERE order_id ?明细查询 / JOIN 关联SQLite 中PRIMARY KEY与UNIQUE会自动建索引idx_item_order是唯一需要手动建的。三把索引覆盖了订单模块的全部高频查询路径——建表时想清楚「哪些列会被 WHERE/JOIN 命中」索引设计就完成了。十二、FAQQ1为什么不用物理外键SQLite 的外键需要每次连接PRAGMA foreign_keys ONrelationalStore 封装下开启繁琐且级联删除的默认行为RESTRICT反而要写额外代码。逻辑外键 应用层两行级联更直白。Q2status 为什么不用字符串 ‘PAID’ 而用数字数字可参与GROUP BY status统计和区间比较字符串只能等值匹配且数字编码天然对应流程顺序。代价是阅读性差用statusText()映射函数弥补。Q3金额 REAL 会不会出错有理论误差但两位小数的场景下Math.round(x * 100) / 100规整后展示无感。生产环境金额敏感系统可考虑「以分为单位存 INTEGER」——本实例为教学清晰选 REAL。Q4order_no 用 Date.now() 会不会撞号毫秒级时间戳在单机单线程下不会撞多设备离线场景可加设备前缀如SO${deviceId}${Date.now()}UNIQUE 约束兜底防重。Q5为什么明细表存 product_name 快照而不存商品 id订单是历史事实。若引用商品表商家改价/改名后历史订单会被「篡改」财务对账就出问题。快照 成交价才是订单明细的正解。Q6查询明细时orderByAsc(id)的意义明细按插入顺序id 自增返回保证商品顺序与下单时一致——UI 展示的明细顺序就是用户加购的顺序。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻