问题:
因项目JDK升级从11升到21,相应的Spring从2.x升级到3.x,Hiberante也从5.x升级到6.6. 然后在测试时发现如下代码报错
实体类User对应表users
@Data
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(generator = "customerUUIDGenerator")
@GenericGenerator(
name = "customerUUIDGenerator",
strategy = "org.example.id.customerUUIDGenerator"
)
private UUID id;
@Version
private int version;
@Column(unique = true, nullable = false)
private String username;
}
自定义id生成器
public class CustomerUUIDGenerator implements IdentifierGenerator {
@Override
public Object generate(SharedSessionContractImplementor session, Object object) {
final EntityPersister entityPersister = session.getEntityPersister( null, object );
Object id = entityPersister.getIdentifier( object, session );
return null != id ? id : UUID.randomUUID();
}
}
代码中存在如下使用
...
public void addUser(UUID id, String name, String passwd) {
User user = new User();
user.setId(id);
user.setUsername(name);
userRepository.save(user);
}
上面代码报错: detached entity passed to persist 或者StaleObjectStateException
原因
Hibernate在保存时会先查数据库有没有该数据
如果无则判断实体是否新创建的,判断流程主要如下
- 先判断id是否为空
- 如果version为null且id为外键
- 依据unsavedStrategy判断,尽在返回false时报错,否则存入数据库
- id == null 则为true
- UnsavedStrategy.isUnsaved返回null或true,而UnsavedStrategy有如下几种
- Null: 在id为null时返回true
- Undefined: 一直返回null
看到这我本以为Hibernate会提供配置让使用方外部制定UnsavedStrategy类型,但google搜了一圈,发现没有。最后是下载Hibernate源码,在其单元测试中看到是通过重写生成器的allowAssignedIdentifiers方法来影响UnsavedStrategy。
流程是
- 创建生成器时判断allowAssignedIdentifiers为true,且id为简单值(Java类型,不是多个类型符合成的自定义对象)则设置NullValue为Undefined
public class SessionFactoryImpl extends QueryParameterBindingTypeResolverImpl implements SessionFactoryImplementor {
...
private static Map<String, Generator> createGenerators(
JdbcServices jdbcServices,
SqlStringGenerationContext sqlStringGenerationContext,
MetadataImplementor bootMetamodel,
BootstrapContext bootstrapContext) {
final Map<String, Generator> generators = new HashMap<>();
bootMetamodel.getEntityBindings().stream()
.filter( model -> !model.isInherited() )
.forEach( model -> {
final KeyValue id = model.getIdentifier();
final Generator generator = id.createGenerator(
bootstrapContext.getIdentifierGeneratorFactory(),
jdbcServices.getJdbcEnvironment().getDialect(),
(RootClass) model
);
if ( generator instanceof Configurable ) {
final Configurable identifierGenerator = (Configurable) generator;
identifierGenerator.initialize( sqlStringGenerationContext );
}
if ( generator.allowAssignedIdentifiers() && id instanceof SimpleValue ) {
final SimpleValue simpleValue = (SimpleValue) id;
if ( simpleValue.getNullValue() == null ) {
simpleValue.setNullValue( "undefined" );
}
}
generators.put( model.getEntityName(), generator );
} );
return generators;
}
...
}
- 在UnsavedValueFacatory中依据NullValue创建UnsavedStrategy
public class UnsavedValueFactory {
/**
* Return the UnsavedValueStrategy for determining whether an entity instance is
* unsaved based on the identifier. If an explicit strategy is not specified, determine
* the unsaved value by instantiating an instance of the entity and reading the value of
* its id property, or if that is not possible, using the java default value for the type
*/
public static IdentifierValue getUnsavedIdentifierValue(
KeyValue bootIdMapping,
JavaType<?> idJtd,
Getter getter,
Supplier<?> templateInstanceAccess) {
final String unsavedValue = bootIdMapping.getNullValue();
if ( unsavedValue == null ) {
... // omit unrelated part
}
else if ( "null".equals( unsavedValue ) ) {
return IdentifierValue.NULL;
}
else if ( "undefined".equals( unsavedValue ) ) {
return IdentifierValue.UNDEFINED;
}
else if ( "none".equals( unsavedValue ) ) {
return IdentifierValue.NONE;
}
else if ( "any".equals( unsavedValue ) ) {
return IdentifierValue.ANY;
}
else {
return new IdentifierValue( idJtd.fromString( unsavedValue ) );
}
}
解决
生成器重写allowAssignedIdentifiers,返回true:
public class CustomerUUIDGenerator implements IdentifierGenerator {
/**
* indicate JPA use undefined type of unsaved strategy
*/
@Override
public boolean allowAssignedIdentifiers() {
return true;
}
@Override
public Object generate(SharedSessionContractImplementor session, Object object) {
final EntityPersister entityPersister = session.getEntityPersister( null, object );
Object id = entityPersister.getIdentifier( object, session );
return null != id ? id : UUID.randomUUID();
}
}
总结
- 最开始在Stackoverflow查找解决方案,但发现StackOverflow很久没跟新了,不知道是不是AI Chat Robot的崛起,很多人都在上面交流了
- 然后用google也没找到,最后还是源码的单测中发现的。
- 收获就是Google没找到就还是立马看下源码的样例或单测吧