ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

若依代码生成器主子表生成:一对多CRUD、外键与关联查询避坑

若依代码生成器主子表生成:一对多CRUD、外键与关联查询避坑 若依代码生成器里最能体现一站式威力的功能我个人认为是主子表生成。单表生成只是省了敲CRUD的力气主子表生成则是直接把你在一对多业务场景里最容易写乱的那部分——外键赋值、批量插入、关联查询、先删后插——整套骨架替你搭好了。我最初接触若依时也走过弯路第一回生成主子表跑起来前端表格一条数据都不显示排查了快两个小时才发现是关联信息里子表外键名填成了主表字段名。这类坑看起来很傻但只要没人提前告诉你谁都得踩一遍。这篇内容适合正在用若依做后台管理系统的开发同学尤其是第一次碰主子表、或者生成完之后发现数据不对想搞清楚原理的人。下面我按生成前的配置、生成出来的代码、跑起来之后的排查、以及后续改造这条完整链路来讲尽量把每个决策背后的原因说透。1. 主子表在若依里到底生成了什么很多人对主子表的理解停留在两张表一起生成但真正有价值的问题是若依替你把哪些本该手写的逻辑固化了搞清楚这一点后面配置和排查才有参照。1.1 一对多关系在代码层面的三个固定动作主子表本质上就是数据库里的一对多关系。以最常见的订单场景为例一张biz_order订单主表一张biz_order_item订单明细子表一个订单对应多条明细明细表通过order_id这个外键指回主表。这种关系在业务代码里无论用什么框架都要做三件事新增主表时把明细逐条挂上主表主键再批量落库查询主表时把它的明细一并捞出来放进同一个对象修改主表时先清掉旧的明细再写新的。这三件事单看都不难难的是每次都写、每次都要保证事务、每次都要处理主键回填的时序问题。若依的主子表生成直接把这三个动作做成了标准模板。新增走的是插主表拿到自增主键再遍历子表列表设置外键最后批量插入修改走的是按外键删除旧子表数据再重新插入新数据查询走的是主表和子表做关联查询用 MyBatis 的 collection 把子表结果集折叠进主表对象的 List 字段。注意这里有个容易被忽略的假设——主表主键必须是插入后能回填到实体的。若依默认按自增主键处理如果你的表用了非自增策略又没配好主键回填子表外键会全是空值。1.2 生成器实际吐出来的文件清单理解清楚这三个动作后再看生成了哪些文件就很清晰了。以业务名order、模块名biz为例生成器会产出下面这些内容。后端 Java 部分文件作用主子表特有之处Order.java主表实体普通字段映射OrderItem.java子表实体含指向主表的外键字段OrderVo.java视图对象继承主表实体多一个ListOrderItemOrderMapper.java/OrderItemMapper.java数据层接口主表接口里多了批量子表操作OrderServiceImpl.java业务层实现新增/修改/查询的主子联动逻辑都在这OrderController.java控制层对外接口基本和单表一致OrderMapper.xml/OrderItemMapper.xml映射文件主表 XML 里有 collection 映射和批量 SQL前端部分主要是两块api/biz/order.js负责请求封装views/biz/order/index.vue是页面。主子表的页面和单表差别很明显——单表是纯列表主子表会在新增/修改的弹窗里嵌一个可编辑的子表表格用户在主表单里填字段在下方的明细表格里一行行加子项。搞清楚了这套生成物的构成你在配置阶段就知道每个配置项会影响哪个文件出错时也大概能猜到是哪个环节的问题而不是对着报错干瞪眼。1.3 为什么主子表比单表更容易翻车单表生成出来基本是开箱即用主子表却经常需要二次调整才能跑通原因在于它引入了两个额外变量外键的对应关系、以及主表子表的字段命名冲突。外键对应关系依赖你在配置时填的关联信息填错就直接查不出数据字段命名冲突则是很多开发者在建表时没刻意规避——主表有id、create_time子表往往也有id、create_time一旦关联查询不做列别名处理MyBatis 映射时列名会打架。这两个变量单表完全没有所以主子表出问题的概率天然更高。2. 生成前那几个配置项错一个后面全返工生成这一步在若依里是系统工具菜单下的代码生成流程看起来就是导入表、改配置、点生成但主子表有几个配置项必须按规矩来不然生成出来的代码是残的。2.1 子表必须先自己导入不能指望主表带出来这是新手最容易忽略的一步。若依的代码生成器里主表和子表是两条独立的记录你导入biz_order之后系统并不会自动把biz_order_item也认出来。正确做法是先在数据库里把两张表都建好然后回到代码生成页面把两张表都执行导入操作这时候列表里会同时出现biz_order和biz_order_item两条待生成记录。只有当子表已经在列表里存在你在编辑主表生成配置、把模板类型切到主子表时关联信息里的子表下拉框才能选到它。如果子表没导入你会发现子表名那一栏根本找不到目标表于是有人就干脆放弃主子表、按单表生成——那后面所有联动逻辑都得自己补。提示建表时给子表起名尽量规范比如主表biz_order、子表biz_order_item或biz_order_detail这样下拉框里一眼就能认出来。名字起得太随意表多了之后很容易选错。2.2 关联信息里的三件套分别是干什么的切到主子表模板后生成配置区会多出一组关联信息字段这是整个主子表生成的核心。不同版本的若依界面措辞略有差异但核心就三样东西。第一样是关联子表的表名也就是要挂到主表下的那张子表从下拉里选。第二样是关联子表的外键名指的是子表里指向主表主键的那个字段名比如子表里的order_id。这里务必填子表里的字段名绝对不能填主表的字段名——我第一回就是在这里填了主表的id结果生成出来的批量插入和外键删除逻辑全是对着主表主键在操作数据自然全乱套。第三样是关联子表显示的字段这个字段决定前端子表表格里展示哪一列一般选子表里最有业务含义的那列比如商品名称或者明细描述。配置项填什么填错的直接后果关联子表表名子表物理表名如biz_order_item选不到表无法生成主子表关联子表外键名子表指向主表的字段如order_id外键赋值失败子表数据挂不到主表关联子表显示字段子表里有业务含义的列如item_name前端明细列表显示为空或显示主键除了关联信息主表和子表各自的字段配置也要顺手过一遍。哪些字段需要作为查询条件、哪些字段在列表里展示、显示类型选输入框还是下拉框这些都会直接影响生成的页面能不能用。字段配置这块单表怎么配主子表就怎么配逻辑是一致的。2.3 主键策略和字段类型要提前对齐还有一个隐藏的坑主表主键和子表外键的类型必须一致。如果主表id是bigint子表order_id却建成int或者varchar关联查询时虽然可能不报错但类型不匹配在某些数据库里会导致隐式转换甚至走不上索引。更麻烦的是子表外键设成字符串之后后端实体里orderId会生成String类型而主表getId()返回Long你在 Service 里给外键赋值时就得手动转若依生成的模板代码可不会替你做这个转换编译直接不过。我一般建议主键统一用bigint自增子表外键也用bigint跟主键类型完全对齐省心。如果你的项目用雪花 ID 或者 UUID主键和外键的 Java 类型也要一致通常都是Long或String二选一别混着来。3. 生成的代码逐层拆解从 Vo 到 XML 再到 Vue配置没问题点生成代码落到工程里。这时候别急着启动项目跑先把生成的几个关键文件看一遍知道若依到底给你写了什么后面改起来心里才有底。3.1 Vo 里的 List 字段和 Service 的先删后插先说OrderVo.java。它继承主表实体Order多加了一个字段public class OrderVo extends Order { private static final long serialVersionUID 1L; private ListOrderItem orderItemList; // getter/setter }这个 List 是整个主子表的数据载体。前端提交过来的 JSON 里主表字段和名为orderItemList的数组一起传Spring 反序列化进这个 Vo主表字段落到父类明细落到 List。理解这一点你就明白为什么前端api里提交的对象结构长那样了。再看OrderServiceImpl里最核心的两个方法。新增方法大致长这样Override Transactional(rollbackFor Exception.class) public int insertOrder(OrderVo order) { int rows orderMapper.insertOrder(order); insertOrderItem(order); return rows; } public void insertOrderItem(OrderVo order) { ListOrderItem orderItemList order.getOrderItemList(); Long id order.getId(); // 主表插入后回填的主键 if (StringUtils.isNotNull(orderItemList)) { ListOrderItem list new ArrayList(); for (OrderItem item : orderItemList) { item.setOrderId(id); // 关键把主表主键赋给子表外键 list.add(item); } if (!list.isEmpty()) { orderMapper.batchOrderItem(list); // 批量插入 } } }这里能看出几个设计意图。Transactional保证了主表插入和子表批量插入在同一事务里任一步失败一起回滚不会出现主表有了、明细没写进去的脏数据。主键回填依赖 MyBatis 的useGeneratedKeys配置在 XML 的 insert 语句里。外键赋值这一步是主子表的灵魂若依替你写好了循环你只要保证配置阶段的外键名填对就行。修改方法则是典型的先删后插Override Transactional(rollbackFor Exception.class) public int updateOrder(OrderVo order) { orderMapper.deleteOrderItemByOrderId(order.getId()); insertOrderItem(order); return orderMapper.updateOrder(order); }先按主表主键删掉所有旧明细再把新提交的明细当成全新的插进去。这个做法逻辑简单、不容易漏缺点是子表主键每次都变如果明细上有历史关联或者审计需求就不能这么粗暴。这个取舍后面第 5 节会展开。3.2 XML 里的 collection 映射和同名字段冲突查询逻辑在OrderMapper.xml里核心是 resultMap 中的 collectionresultMap typeOrderVo idOrderResult result propertyid columnid/ result propertyorderNo columnorder_no/ collection propertyorderItemList ofTypeOrderItem result propertyid columnitem_id/ result propertyorderId columnorder_id/ result propertyitemName columnitem_name/ /collection /resultMapcollection 告诉 MyBatis主表查出来的每一行如果id相同就把子表部分折叠成一条OrderItem塞进 List。这就解释了为什么一条订单能带出多条明细——关联查询会返回 N 行MyBatis 负责把相同主键的行合并。而这里的列名就是后文反复强调的冲突点。主表有id子表也有id如果查询 SQL 里两列都叫idMyBatis 根本分不清哪个给主表、哪个给子表。若依较新版本生成时会对子表列做别名处理你对照生成的 SQL 看一眼就知道它有没有处理好。如果发现别名没加实体的子表id会取到主表的id导致明细行的主键全是错的。注意验证这个坑最快的办法是造一条有 3 条明细的测试数据调一下详情接口看返回的orderItemList里每条的id是否各不相同、是否等于子表真实主键。如果全等于主表 id就是列名冲突没处理。3.3 前端页面的明细表格怎么接数据前端index.vue里新增和修改共用一个弹窗弹窗上半部分是主表字段表单下半部分是子表表格。子表表格绑定的数据源就是form.orderItemList这个数组。加一行就是往数组 push 一个空对象删一行就是 splice 掉。保存时整个 form 直接提交后端靠 Vo 的 List 接收。这套前端逻辑本身不复杂但有两个细节要留意。第一子表表格每行的输入框如果用v-model绑定要确保绑定的是当前行的对象用scope.row那套写法否则快照式的绑定会导致改一行影响多行。第二明细行的删除按钮只是把数据从数组里移除真正落库的删除发生在后端先删后插的逻辑里前端不需要单独调删除接口。4. 跑起来之后的典型报错与排查代码生成完、编译通过、页面能打开不代表业务就对了。下面这几个问题是主子表实战里出现频率最高的我把排查链路完整写出来。4.1 详情接口返回的明细列表是空的现象是列表页能查到主表数据但点进详情或者编辑弹窗里明细表格一条都不显示。这个问题的根因通常在关联查询这一环。第一步直接去数据库里执行若依生成的selectOrderById那条 SQL看有没有子表数据返回。如果 SQL 本身查不出东西问题在数据或外键值上——很可能是外键字段是空的说明当初新增时外键就没赋上值回头检查配置阶段的外键名是否填成了子表真正的字段。第二步如果 SQL 能查出数据但接口返回的 List 是空那基本是列名冲突或者 collection 的ofType路径写错了。逐行比对 resultMap 里的列名和 SQL 里 select 出来的列名能对上基本就解决了。4.2 新增主表成功但明细没落库这种现象分两种。一种是主表插进去了子表一条没有。先看后端日志有没有执行批量插入的 SQL如果没有说明orderItemList是空的——前端根本没传上来去 Network 里看请求体确认明细数组的字段名和 Vo 里的 List 字段名完全一致大小写都不能差。另一种是批量插入 SQL 执行了但报了错被回滚日志里通常能看到外键字段为空或者类型不匹配。这时候回到实体类和表结构对比外键字段的 Java 类型和数据库类型是否一致。加一句Transactional之上的日志打印把准备批量插入的 list 内容打出来是最快定位的方式别靠猜。4.3 编辑保存后子表数据翻倍或者错乱如果编辑一次明细从 3 条变成 6 条通常是没有真正执行先删后插或者删除条件用错了字段。检查deleteOrderItemByOrderId这条 SQL 的 where 条件它应该按主表主键删。如果配置阶段外键名填错删除条件会挂到错误字段上删不掉旧数据新的又插进去自然翻倍。还有一种情况是多用户并发编辑同一订单两个请求几乎同时进来先删后插之间产生交叉导致明细重复或者丢失。这种并发问题单靠先删后插解决不了需要加锁或者改成差量更新属于进阶话题。4.4 页面报 500 但日志看不出明确错误偶尔会遇到列表接口直接 500但异常栈指向某个类型转换。这往往是主表子表字段命名冲突导致的映射异常比如子表某个字段类型和主表同名字段类型不同MyBatis 映射时试图把两个不同类型的值塞进同一个属性。解决办法是把子表字段在查询里全部加别名让主表和子表的列名彻底区分开。养成建表时主表子表字段不重名的习惯能从源头避免这类问题——子表字段统一加业务前缀比如item_name而不是name是成本最低的预防手段。5. 从生成到落地二次改造的几个实用姿势生成的代码是骨架真实项目里几乎都要再改。下面这几个改造方向是我用得比较多的分享具体的做法和取舍。5.1 把先删后插改成差量更新先删后插最大的问题是子表主键每次都变如果明细上有审批状态、附件、操作日志之类需要保留历史的数据就不能这么干。改造思路是给子表数据加上标记前端提交时已存在的明细行带着它的子表主键新增的行不带。后端在保存时先把没带主键的新行插入再对带主键的行执行更新最后把这次没出现过的旧行删除。这个过程比先删后插复杂但能保住子表主键的稳定性。若依生成的是基础版差量更新需要你自己实现通常在 Service 里加一段比对逻辑。5.2 给明细行加上表单校验生成出来的前端明细表格默认没有校验用户可能一行都不填就保存或者填了半行。要加校验可以在子表表格的输入框上用 Element 的表单校验但更省事的做法是在后端做。在insertOrderItem之前遍历 list过滤掉完全空白的行对必填字段做非空判断。我一般两处都做前端做即时提示后端兜底防绕过。5.3 明细数据量大时的查询优化如果一个主表能挂几百上千条明细关联查询返回的行数会很大MyBatis 折叠成 List 时内存和耗时都上去了。这时候可以考虑两点一是详情接口不要一次性把明细全查出来改成分页二是列表页压根不查明细只有进详情才查。若依生成的列表查询selectOrderList默认只查主表这本身是对的别手贱去给它加 collection。另外给子表的外键字段建索引是提升关联查询性能最直接的一招。5.4 主子表的导出怎么做若依自带 Excel 导出但默认导出的只是主表。主子表导出通常有两种形态一种是平铺一个订单有多条明细就导出多行主表字段重复另一种是分组每个订单一块下面跟着明细。平铺相对好实现在导出逻辑里先把主表按 ID 查出明细再拼成一行行数据。分组导出需要自己控制 Excel 的合并单元格工作量大一些。选哪种取决于使用方是谁——给财务对账用平铺更友好给管理层看用分组更清晰。6. 我反复用到的几条经验最后分享几条我在实际项目里反复用到的经验都是踩过坑之后才明白的。第一条建表阶段就把命名规范定死。主表主键统一id子表外键统一主表驼峰_id比如biz_order对应orderId子表其他字段全部加业务前缀避免和主表重名。这一步花十分钟能省掉后面调试的两个小时。命名混乱是主子表绝大多数奇怪 bug 的根源。第二条生成完先别急着改先老老实实跑一遍生成的原始代码。很多人拿到代码就想按自己的习惯重构结果把若依本来自洽的逻辑改断了再想对照问题都难。正确顺序是先让生成的代码在本地完整跑通一次增删改查确认主子联动没问题再开始改造。有了这个能跑通的基线后续改出问题还能回退对比。第三条调试主子表数据库里的数据是最好的线索。接口返回不对时先去库里直接看子表的外键列有没有值、值对不对。外键是主子表的命脉外键对了一切都好查外键错了一切都白搭。与其盯着后端日志发呆不如先看一眼数据库。第四条子表数据的所有操作都确认在同一个事务里。若依生成的 Service 方法默认带了Transactional但如果你二次改造时把批量插入的方法拆出去单独调用别忘了事务传播级别。我见过一次主表插入成功、子表插入失败却没回滚的事故就是改造时把方法拆散了、事务边界搞没了。第五条并发场景下先想清楚要不要锁。单人操作用的系统先删后插完全够用。但如果是多人协作、同一主表可能被多人同时编辑的系统就得考虑加乐观锁或者把明细操作改成差量更新。这个问题在代码生成阶段不会暴露上线之后才可能冒出来提前想一步能省很多事。用了这么久的若依主子表生成我最大的感受是它解决的是重复劳动而不是复杂逻辑。配置对了、命名规范了生成出来的东西能覆盖八成以上的常规一对多场景剩下的两成靠二次改造补齐。把生成的代码当做一个可靠的起点而不是一个需要推翻的半成品心态上会轻松很多。
返回列表