@ManyToMany,Cascade为All时,save报detached entity passed to persist

问题描述

public class EditPrivilege extends EntityId {
    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinTable(name = REL_EDITPRIVILEGE_ROLE)
    private Set<Role> editableRoles = new HashSet<>();

    @Column
    private String modelName;
}

当应用启动,自动建表并导入数据,执行repository.save()方法报detached entity passed to persist: edu.ecnu.yjsy.model.auth.Role错误。其中Role已经存入到数据表,目的是想添加EditPrivilege以及中间表的数据。

问题原因

既然报错了,那就先让我看看抛出这个Exception的源码吧。

public void onPersist(PersistEvent event, Map createCache) throws HibernateException {
        // ....省略一些代码

        // 下面我们看到entityState的值为DETACHED,就是从这里获得的
        EntityState entityState = getEntityState( entity, entityName, entityEntry, source );

        if ( entityState == EntityState.DETACHED ) {
            EntityPersister persister = source.getFactory().getEntityPersister( entityName );

            // 因为在区别PERSISTENT与DETACHED的时候通过ForeignKeys.isTransient来判断,
            // 为了防止用户自定义的id生成策略使得框架发送误判,这里对自定义id策略的情况再次获取entityState
            if ( ForeignGenerator.class.isInstance( persister.getIdentifierGenerator() ) ) {
                persister.setIdentifier( entity, null, source );
                entityState = getEntityState( entity, entityName, entityEntry, source );
            }
        }

        switch ( entityState ) {
            case DETACHED: { // 错误来源于这里,说明现在需要保存对象的entityState为Detached
                throw new PersistentObjectException(
                        "detached entity passed to persist: " +
                                getLoggableName( event.getEntityName(), entity )
                );
            }
            // 省略....
        }

    }

从上面的源码中看到,当前实体的entityState被判断为DETACHED,下面我们看getEntityState方法:

/**
     * Determine whether the entity is persistent, detached, or transient
     *
     * @param entity The entity to check
     * @param entityName The name of the entity
     * @param entry The entity's entry in the persistence context
     * @param source The originating session.
     *
     * @return The state.
     */
    protected EntityState getEntityState(
            Object entity,
            String entityName,
            EntityEntry entry, //pass this as an argument only to avoid double looking
            SessionImplementor source) {

        //省略...

        // 就是下面这行判断出当前实体的entityState为DETACHED,getAssumedUnsaved()返回null
        if ( ForeignKeys.isTransient( entityName, entity, getAssumedUnsaved(), source ) ) {
            return EntityState.TRANSIENT;
        }
        return EntityState.DETACHED;
    }

让我们在往下面看ForeignKeys.isTransient方法

/**
     * Is this instance, which we know is not persistent, actually transient?
     * <p/>
     * If <tt>assumed</tt> is non-null, don't hit the database to make the determination, instead assume that
     * value; the client code must be prepared to "recover" in the case that this assumed result is incorrect.
     *
     * @param entityName The name of the entity
     * @param entity The entity instance
     * @param assumed The assumed return value, if avoiding database hit is desired
     * @param session The session
     *
     * @return {@code true} if the given entity is transient (unsaved)
     */
    public static boolean isTransient(String entityName, Object entity, Boolean assumed, SessionImplementor session) {
        // 如果当前实体是LAZY的,返回false
        if ( entity == LazyPropertyInitializer.UNFETCHED_PROPERTY ) {
            return false;
        }

        // 让拦截器去检查
        Boolean isUnsaved = session.getInterceptor().isTransient( entity );
        if ( isUnsaved != null ) {
            return isUnsaved;
        }

        // 让persister来检查,经过调试,就是这里返回false,让我们看persister.isTransient是如何判断的
        final EntityPersister persister = session.getEntityPersister( entityName, entity );
        isUnsaved = persister.isTransient( entity, session );
        if ( isUnsaved != null ) {
            return isUnsaved;
        }

        // 因为上面getAssumedUnsaved()返回的值为null,所以这里不返回
        if ( assumed != null ) {
            return assumed;
        }

        //上述方法都不行,则访问数据库来判断当前实体的状态
        final Object[] snapshot = session.getPersistenceContext().getDatabaseSnapshot(
                persister.getIdentifier( entity, session ),
                persister
        );
        return snapshot == null;

    }

persister.isTransient方法

public Boolean isTransient(Object entity, SessionImplementor session) throws HibernateException {
        final Serializable id;
        if ( canExtractIdOutOfEntity() ) {
            id = getIdentifier( entity, session );
        }
        else {
            id = null;
        }
        // 当一个实体的id为null,就认为它是Transient
        if ( id == null ) {
            return Boolean.TRUE;
        }

        // 判断这个实例是否有时间戳或者版本号控制
        if ( isVersioned() ) {
            // 如果是,则判断实体的version有没有保存过,如果保存过,那返回false
            Boolean result = entityMetamodel.getVersionProperty()
                    .getUnsavedValue().isUnsaved( version );
            if ( result != null ) {
                return result;
            }
        }

        // 判断当前实体的id是否保存过,保存过则表示当前实体为Detached,返回false。
        // 就是在这里,Role这个类的entityMetamodel.getIdentifierProperty().getUnsavedValue()为0,但是实体的id值不为0。

        Boolean result = entityMetamodel.getIdentifierProperty()
                .getUnsavedValue().isUnsaved( id );
        if ( result != null ) {
            return result;
        }

        // check to see if it is in the second-level cache
        if ( session.getCacheMode().isGetEnabled() && hasCache() ) {
            final EntityRegionAccessStrategy cache = getCacheAccessStrategy();
            final Object ck = cache.generateCacheKey( id, this, session.getFactory(), session.getTenantIdentifier() );
            final Object ce = CacheHelper.fromSharedCache( session, ck, getCacheAccessStrategy() );
            if ( ce != null ) {
                return Boolean.FALSE;
            }
        }

        return null;
    }

结论

后来发现问题是因为设置了Cascade.All,当执行save的时候,会调用onPersist()方法,这个方法会递归调用外联类(即Role)的onPersist()进行级联新增,但是roles已经添加了,因此报detached entity passed to persist。后来我将级联操作取消就ok了(其实只要将cascadeType.persist去掉就可以)。

所以当我们设计模型之间的级联关系的时候,要考虑好应该采用何种级联规则。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值