Spring JPA带id保存时报错:detached entity passed to persist

404 阅读2分钟

问题:

因项目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在保存时会先查数据库有没有该数据

如果无则判断实体是否新创建的,判断流程主要如下

image.png

  1. 先判断id是否为空
  2. 如果version为null且id为外键
  3. 依据unsavedStrategy判断,尽在返回false时报错,否则存入数据库
  • id == null 则为true
  • UnsavedStrategy.isUnsaved返回null或true,而UnsavedStrategy有如下几种
    • Null: 在id为null时返回true
    • Undefined: 一直返回null

看到这我本以为Hibernate会提供配置让使用方外部制定UnsavedStrategy类型,但google搜了一圈,发现没有。最后是下载Hibernate源码,在其单元测试中看到是通过重写生成器的allowAssignedIdentifiers方法来影响UnsavedStrategy。 流程是

  1. 创建生成器时判断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;
}

...
}

  1. 在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();
    }
}

总结

  1. 最开始在Stackoverflow查找解决方案,但发现StackOverflow很久没跟新了,不知道是不是AI Chat Robot的崛起,很多人都在上面交流了
  2. 然后用google也没找到,最后还是源码的单测中发现的。
  3. 收获就是Google没找到就还是立马看下源码的样例或单测吧