Spring Data @Version属性的实体状态检测对基本类型不生效
问题分析与解答
你对官方文档的理解确实和Spring Data JPA的实际执行逻辑存在偏差——官方文档的描述不够精准,框架的源码逻辑才是实际执行的依据。
核心矛盾点
官方文档提到:存在@Version注解的属性时,若属性为null,或是基本类型且值为0时,实体被视为新实体。但实际查看JpaMetamodelEntityInformation类的isNew方法源码:
@Override public boolean isNew(T entity) { if (versionAttribute.isEmpty() || versionAttribute.map(Attribute::getJavaType).map(Class::isPrimitive).orElse(false)) { return super.isNew(entity); } BeanWrapper wrapper = new DirectFieldAccessFallbackBeanWrapper(entity); return versionAttribute.map(it -> wrapper.getPropertyValue(it.getName()) == null).orElse(true); }
当version字段是基本类型(比如int)时,代码会直接调用super.isNew(),而这个父类方法的逻辑仅检查ID字段是否为null:如果你手动设置了ID,父类方法会判定实体不是新的,进而触发em.merge()(会先执行额外的查询语句)。
为什么源码这么设计?
基本类型的version字段有个天然限制:它不能为null,默认值是0。但0本身可能是合法的版本号(比如某些已存在的实体版本值就是0),如果框架用“基本类型值为0”来判定新实体,会出现误判——把已存在的版本为0的实体当成新实体处理。
而包装类型(比如Integer)可以为null,新实体的version默认是null,已存在的实体version有具体数值,框架可以安全地用“version是否为null”来区分新老实体,这也是改成Integer后问题解决的原因。
验证与解决方案
- 你的测试结果完全正确:用
int类型的version时,手动设置ID会触发merge();改成Integer后,只要version为null,即使设置了ID,框架也会判定为新实体,执行persist()。 - 如果必须使用基本类型的version字段,你可以自定义
isNew逻辑:让实体实现Persistable接口,重写isNew()方法,比如通过判断version是否为0来识别新实体:
@Entity @Table(name = "test") public class Test implements Serializable, Persistable<Long> { @Serial private static final long serialVersionUID = 1L; @Id private Long id; @Version @NotNull @Column(name = "entity_version") private int entityVersion; @Override public boolean isNew() { return entityVersion == 0; } @Override public Long getId() { return this.id; } }
内容的提问来源于stack exchange,提问作者Caffadras
相关产品推荐
相关产品推荐

