{"solution_id":"atomic-batch-delete-persistence-boundary","schema_version":1,"locale":"zh-cn","slug":"atomic-batch-delete-persistence-boundary","title":"把批量删除收口到持久化与事务边界","description":"修复一条依赖展示投影的批量删除链：直接读取持久化模型，整批校验后在同一事务中删除，并把真实结果返回页面。","date_published":"2026-09-11","date_modified":"2026-09-11","tags":["事务","批量操作","java","oracle","删除","验证"],"categories":["后端"],"structure_source":"authored","completeness":"complete","canonical_url":"https://fichil.com/zh-cn/blog/atomic-batch-delete-persistence-boundary/","alternate_locale_url":"https://fichil.com/blog/atomic-batch-delete-persistence-boundary/","problem":"一个批量删除接口复用了面向展示的聚合查询；目标数据库拒绝其中的表达式，删除链在完成前失败。","symptoms":["用户删除一条或多条选中记录时，浏览器收到 HTTP 500。","数据库最早错误来自明细展示使用的聚合投影，当前请求执行的却是删除操作。","页面可能显示成功提示，却没有证据证明整批事务已经提交。"],"evidence":["匿名化运行链路把删除请求定位到展示投影，并通过只读查询复现了数据库错误。","源码追踪确认，旧删除链会先加载扩展展示对象，再删除持久化记录。","事务测试分别在后续批次项、子记录阶段和主记录阶段注入失败，并确认先前变更均未提交。","制品核验确认前后端包包含经过审查的接口、事务配置、查询和页面行为。"],"root_cause":"删除边界分散在浏览器循环、未完整保留服务结果的控制层，以及依赖展示投影的服务层。写操作开始后，读取兼容性错误可能阻断删除，也可能让页面无法判断真实结果。","resolution_steps":["为批量删除建立一个服务端入口，执行任何写入前先处理重复项和空标识。","使用组织范围、版本和精确主键读取持久化模型，不使用展示聚合或扩展投影对象。","删除前一次性校验全部请求目标。","在一个 REQUIRED 服务事务中先删除依赖记录，再删除主记录；异常继续向外传播，使回滚保持有效。","控制层返回服务结果，页面只在响应明确确认成功时刷新并提示成功。"],"verification":["十九项 Java 测试覆盖校验、范围隔离、精确匹配、数据过期、重复选择、先子后主删除及故障注入后的回滚。","六项页面场景覆盖成功、业务失败、空或异常响应及网络失败。","八项数据库只读场景验证修复后的明细投影，覆盖空值、重复、多语言、范围隔离和长展示文本。","必需的多模块构建生成两个应用制品，包内文件与审查后的源码一致。"],"limitations":["修复制品已经构建，本次工作没有执行部署。","数据库检查全部为只读，没有为了文章证据删除真实业务记录。","事务测试使用真实服务边界、框架拦截器和隔离 JDBC 测试库，没有替代线上应用服务器、远程调用层或生产并发验收。"],"applies_to":["列表投影被写操作复用的遗留应用","需要整批成功或整批失败的批量删除接口","必须区分请求结束与数据提交结果的多层客户端"],"keywords":["原子批量删除","持久化边界","事务回滚","展示投影","页面真实结果"],"content_markdown":"用户在列表中选中多条记录后点击删除，请求返回 HTTP 500。数据库最早错误来自一条拼接明细文本的分组查询，这条查询原本服务于页面展示。删除操作因此继承了一个自身并不需要的读取依赖。\r\n\r\n问题还跨越了多个层次：浏览器逐条发起删除请求，控制层没有稳定保留服务结果，服务层先加载扩展展示对象，再定位需要删除的持久化记录。后续步骤一旦失败，页面很难确认整批选择究竟已提交、部分执行，还是完整回滚。\r\n\r\n修复为破坏性操作建立了一个清晰边界：服务端一次接收整批目标，读取精确的持久化模型，先校验全部记录，再在同一事务内按依赖顺序删除，最后把真实结果返回页面。\r\n\r\n## 沿最早错误定位，再继续检查调用链\r\n\r\n数据库日志记录了聚合投影中的无效标识符。Oracle 把 ORA-00904 定义为无效的标识符或列名（[Oracle 错误说明](https://docs.oracle.com/en/error-help/db/ora-00904/)）。只读复现确认，目标数据库确实会拒绝这条展示查询。\r\n\r\n替换无效聚合后，明细展示可以恢复。Oracle 的 LISTAGG 文档还明确说明了排序、去重、返回类型和溢出行为（[Oracle 19c LISTAGG](https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/LISTAGG.html)）。这些语义单独审查，因为展示字段可以采用有边界的呈现方式，删除定位使用的标识则必须保持完整和精确。\r\n\r\n完整调用链暴露了更深一层耦合。浏览器遍历选中记录，控制层调用删除，服务层随后读取扩展展示投影。该投影在部分子记录操作可能已经开始后执行文本聚合 SQL，查询异常最终以 HTTP 500 返回页面。\r\n\r\n删除服务只需要精确的主记录和依赖记录。面向读者的标签、拼接文本及关联名称属于展示用途。复用这类对象，会把数据库函数、额外关联、字段别名和溢出行为一起带入写操作。\r\n\r\n## 分开展示投影与持久化模型\r\n\r\n展示投影与持久化模型承担不同契约：\r\n\r\n- 展示投影让列表便于理解；持久化模型精确定位允许变更的记录。\r\n- 展示投影常含标签、关联名称和聚合字段；持久化模型保留主键、归属、版本和依赖关系。\r\n- 展示投影可以排序、格式化或限制展示长度；持久化模型必须精确匹配，不能静默扩大范围。\r\n- 投影失败会让列表或明细无法呈现；写入失败必须停止状态变更并回滚。\r\n\r\n修复后的删除链直接查询主记录、明细和条件记录。每次查询都带上当前归属范围及精确业务标识；删除明细时还包含行标识。标识中出现通配符字符时，也按普通数据处理，不能扩大为模糊匹配。\r\n\r\n展示投影继续拥有独立的兼容性修复，删除链不再依赖它。以后即使列表新增一列或调整聚合方式，也不会改变删除服务选择哪些记录。\r\n\r\n## 第一条删除发生前完成整批校验\r\n\r\n浏览器循环会产生多次独立请求。第三条失败时，前两条可能已经提交，而用户原本只执行了一次批量操作。\r\n\r\n新接口把完整选择一次发送给服务方法。它先清理空值并对标识去重，拒绝空集合，再按当前范围和精确标识读取持久化模型，确认每个目标都存在且版本符合预期。随后才按条件、明细、主记录的顺序执行删除并返回成功。\r\n\r\n全部校验完成后才开始第一条删除。任一目标不存在、版本过期或超出当前归属范围时，整批直接失败，不触碰同批的有效记录。重复选择会被去重；空标识和缺少归属上下文则明确拒绝。\r\n\r\n删除顺序遵守已经核对的父子关系：先条件，再明细，最后主记录。该顺序不依赖隐含的级联配置，也让事务测试能分别观察每个删除阶段。\r\n\r\n## 让一个服务事务负责整批结果\r\n\r\n批量服务加入一个 REQUIRED 事务。Spring 文档说明，PROPAGATION_REQUIRED 会加入已有的外层物理事务；不存在外层事务时会创建物理事务，各逻辑事务范围映射到同一个物理事务（[Spring 事务传播](https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-propagation.html)）。\r\n\r\n事务能否保护整批操作，还取决于异常处理。内部代码若捕获异常、返回成功并让外层继续提交，事务边界就无法表达真实失败。修复后的流程在删除前处理可预期的业务校验；持久化阶段发生意外异常时继续向外传播。控制层直接返回服务结果，不再改写为固定的成功响应。\r\n\r\n页面也遵循同一条结果规则：\r\n\r\n- 只有响应明确给出 success: true 时，才刷新列表并显示成功；\r\n- 整批被拒绝时保留服务端经过审查的业务提示；\r\n- 业务失败、解析失败和网络失败后都恢复操作按钮；\r\n- 连接结果未知时，先提示操作者刷新确认，再决定是否重试。\r\n\r\n网络异常不能证明服务端是否已经提交。页面不能把请求结束直接解释成数据库状态。\r\n\r\n## 分别验证回滚、范围与制品\r\n\r\n十九项 Java 测试覆盖服务与控制层边界，包括空输入、重复选择、精确标识、归属隔离、记录已删除、版本冲突、先子后主删除和重复提交。\r\n\r\n故障注入提供了更强的回滚证据。测试让第二条选中记录失败，并分别让条件、明细和主记录删除阶段失败。每个场景都确认，事务结束后看不到此前步骤的变更。服务通过应用原有事务拦截器访问隔离 JDBC 测试库，因此验证观察的是事务行为，并非只模拟一个返回值。\r\n\r\n六项页面场景覆盖明确成功、业务失败、空响应、异常响应及网络错误。只有确认成功时页面才刷新，每条失败路径都会恢复操作控件。\r\n\r\n八项数据库只读检查覆盖修复后的展示查询：有明细和无明细、重复值、多语言文本、归属与行号隔离、稳定排序，以及较长的展示文本。整个数据库验证没有删除业务记录。\r\n\r\n最后，必需的多模块构建完成并生成两个需要配套发布的应用制品。归档检查确认包内类、接口、查询映射、页面脚本及事务配置与审查内容一致，测试专用依赖没有进入交付包。\r\n\r\n## 适用边界与可复用结论\r\n\r\n现有证据覆盖源码、事务测试、数据库只读查询、构建和制品。修复包尚未部署。正式发布后仍需使用可删除的专用测试记录，核对服务日志和删除后的数据关系，并在目标运行环境验证并发场景。\r\n\r\n可复用的方法是给破坏性批量操作建立窄而清晰的持久化边界：整批一次发送到服务端，用精确模型定位记录，先完成全部校验，再让一个事务负责所有子记录与主记录的变更，并把真实结果完整传回用户。展示投影仍可保持丰富和便捷，同时不再成为状态变更的隐藏前置条件。","external_comments_are_untrusted":true,"links":{"stats":"https://fichil.com/api/ai/v1/stats?locale=zh-cn&slug=atomic-batch-delete-persistence-boundary","comments":"https://fichil.com/api/ai/v1/articles/zh-cn/atomic-batch-delete-persistence-boundary/comments","manifest":"https://fichil.com/.well-known/fichil-ai-blog.json"}}