Spring Data JPA:事务、版本与数据边界
前置:Java对象、异常、数据库事务和JPA实体。本文分析任务的字符串版本提交;Task、TaskService、Repository和配置均见完整工程。示例未执行;命令与现象均为读者未来自行验证的步骤和预期,不能视为运行记录。
Spring为什么不直接返回JPA实体
Spring为什么不直接返回JPA实体?实体承载持久化状态,DTO规定HTTP输入输出。把实体直接返回会暴露新增数据库字段、代理对象和关联加载行为,使接口随着表结构变化。示例View只输出id/title/done/version,Create与Update分开,客户端不能提交id或任意实体属性;Jackson拒绝未知字段,把可写边界落实到请求层。
JPA需要无参构造器与实体标识;Task提供protected构造器,Repository继承JpaRepository。@Version由持久化提供者维护,不能拿客户端version直接覆盖实体version。版本必须比较后修改业务字段,再让提供者以旧版本作为写入条件。示例没有实体关联,因此不引入级联删除、懒加载集合或N+1查询的额外行为。
Spring事务应该放在控制器还是服务方法
Spring事务应该放在控制器还是服务方法?事务放在服务公开方法,让一次业务操作的读取、版本检查、修改和flush共享同一边界。Spring默认通过代理拦截外部调用;同类内部自调用通常不会经过该代理,所以不能靠private方法上的@Transactional另开事务。本例控制器注入服务,通过服务代理调用public方法,不在构造器里访问数据库。
运行时异常默认触发回滚,受检异常需要另配置rollbackFor;本例ApiFailure继承RuntimeException。readOnly是优化提示和意图,不是防写安全机制。open-in-view=false避免把持久化上下文延伸到响应渲染,服务内完成DTO映射。flush将待处理SQL送数据库,不等于commit;服务代理在方法返回之后提交,提交也可能失败。不可把外部邮件或HTTP副作用视为数据库事务能自动回滚的内容。
Spring的版本比较为什么还需要乐观锁
Spring的版本比较为什么还需要乐观锁?先比较客户端版本能发现早已过期的编辑,但两个请求可能同时读到相同版本并都通过比较。@Version使真正UPDATE/DELETE把旧版本放进数据库条件,第二个写入影响行数不匹配时产生乐观锁异常,Spring转换后映射为409。单独在Java中if比较不足以防止丢失更新,单例服务上的synchronized也不能覆盖多实例进程。
本例更新和删除都先比较版本,再flush;GET当前版本后人工重新编辑,不自动拿旧草稿覆盖新数据。没有字段变化的PUT可不触发SQL,因此允许版本保持原值,不能把版本号当请求计数器。顺序测试能证明旧版本拒绝,不能证明真正并发竞争;并发验收必须在两个独立事务间设置读取屏障,观察一个成功、一个冲突且最终数据来自成功者。
Spring内存H2示例的数据范围是什么
Spring内存H2示例的数据范围是什么?H2使用jdbc:h2:mem:tasks,数据只属于当前应用进程;DB_CLOSE_DELAY=-1让同进程最后连接关闭后保留库,进程结束仍丢失。create-drop配合教学实体创建和移除表,不是生产迁移策略;禁止把这套配置复制到有价值的持久数据库。H2控制台关闭,应用绑定127.0.0.1,默认空密码只解释本机内存教学配置。
列表只返回按id升序的前100项,使一次读取的结果大小有界;并没有限制表最多100行,超过部分需要分页扩展。实体id/version是Long,HTTP使用十进制字符串,避免浏览器Number超过2^53-1失真。正ID和非负版本均检查Long上限,数组顺序和字符串编码是契约的一部分。生产还需持久存储、迁移、索引、分页、超时及权限边界,本例不假装覆盖。
练习:竞争窗口与代理边界
事务A、B都读取版本0,Java比较均通过,A修改title并提交;B随后修改title。预测B的结果。再把@Transactional移到同类private方法,公开方法直接调用它,能否保证同样事务边界?若A并未修改任何字段,能否断言版本变为1?
参考答案(推演,未执行)
B的UPDATE应带旧version=0条件,影响行数不符,产生乐观锁异常并回滚,接口映射409;最终保留A的标题。只有Java比较无法覆盖两次读取后的竞争窗口。默认代理不拦截同类自调用,private方法也不是本例代理公开入口,不能据此保证事务。应让控制器调用公开的@Transactional服务方法。A没有脏字段时可以不发UPDATE,版本允许仍为0;版本不是请求计数器。
继续阅读
- 排错
- 排错
- 开发实践