美文网首页Java 杂谈
CascadeType.PERSIST 无法级联保存数据 源码级

CascadeType.PERSIST 无法级联保存数据 源码级

作者: 蔺荆门 | 来源:发表于2018-11-24 18:18 被阅读0次

    前言

    在业务开发中,经常遇到主键ID不能使用自增,而需要使用随机字符串的情况。但是在这种情况下,CascadeType.PERSIST级联保存就有问题了。这里我假设大家知道几种CascadeType是什么意思。话不多提,开始探究

    背景

    Parent表和Child表, 单向一对多关系@OneToMany

    目的

    保存Parent时级联保存Child

    Entity配置

    • Parent
    @Getter //lombok,下同
    @Setter
    @Entity
    public class Parent {
    
        @Id
        @Column(nullable = false, length = 32)
        private String id;
    
        //默认的配置项不再重复写
        //例如OneToMany中的fetch默认为FetchType.LAZY
        //JoinColum中的referencedColumnName默认为Parent的主键
        @OneToMany(cascade = CascadeType.PERSIST)
        @JoinColumn(name = "parentId")
        private List<Child> childList;
    }
    
    • Child
    @Getter
    @Setter
    @Entity
    public class Child {
    
        @Id
        @Column(nullable = false, length = 32)
        private String id;
    
        @Column(length = 32)
        private String parentId;
    }
    

    两个表的主键ID都使用了String类型。到此Entity写完了,如果配置中的spring.jpa.hibernate.ddl-auto你设置为updatecreate的话启动应用之后数据库中就有如下两个表了

    image
    image

    测试级联保存

    public void create() {
        Parent parent = new Parent();
        parent.setId(RandomStringUtils.randomAlphabetic(32));
    
        List<Child> childList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            Child child = new Child();
            child.setId(RandomStringUtils.randomAlphabetic(32));
            //不用设置parentId哦
            childList.add(child);
        }
    
        parent.setChildList(childList);
        parentRepo.save(parent);
    }
    

    代码很简单,不解释了。重头戏要来了,运行!

    org.springframework.orm.jpa.JpaObjectRetrievalFailureException: Unable to find com.sh.blog.entity.Child with id qfHfYhPxvwMEfadUFLLkuXwQGdUDsJCG; nested exception is javax.persistence.EntityNotFoundException: Unable to find com.sh.blog.entity.Child with id qfHfYhPxvwMEfadUFLLkuXwQGdUDsJCG
    
        at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:389)
        at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)
        at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:525)
        at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:59)
        at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:209)
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:147)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:133)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.data.repository.core.support.SurroundingTransactionDetectorMethodInterceptor.invoke(SurroundingTransactionDetectorMethodInterceptor.java:57)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
        at com.sun.proxy.$Proxy86.save(Unknown Source)
        at com.sh.blog.repository.ParentRepoTest.create(ParentRepoTest.java:37)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
        at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
        at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:252)
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:94)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
        at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
        at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
        at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:191)
        at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
        at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
        at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
        at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
        at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
    Caused by: javax.persistence.EntityNotFoundException: Unable to find com.sh.blog.entity.Child with id qfHfYhPxvwMEfadUFLLkuXwQGdUDsJCG
        at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl$JpaEntityNotFoundDelegate.handleEntityNotFound(EntityManagerFactoryBuilderImpl.java:144)
        at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:227)
        at org.hibernate.event.internal.DefaultLoadEventListener.proxyOrLoad(DefaultLoadEventListener.java:278)
        at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:121)
        at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:89)
        at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1129)
        at org.hibernate.internal.SessionImpl.internalLoad(SessionImpl.java:1022)
        at org.hibernate.type.EntityType.resolveIdentifier(EntityType.java:639)
        at org.hibernate.type.EntityType.resolve(EntityType.java:431)
        at org.hibernate.type.EntityType.replace(EntityType.java:330)
        at org.hibernate.type.CollectionType.replaceElements(CollectionType.java:518)
        at org.hibernate.type.CollectionType.replace(CollectionType.java:663)
        at org.hibernate.type.AbstractType.replace(AbstractType.java:147)
        at org.hibernate.type.TypeHelper.replaceAssociations(TypeHelper.java:261)
        at org.hibernate.event.internal.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:427)
        at org.hibernate.event.internal.DefaultMergeEventListener.entityIsTransient(DefaultMergeEventListener.java:240)
        at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:301)
        at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:170)
        at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:69)
        at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:840)
        at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:822)
        at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:827)
        at org.hibernate.jpa.spi.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:1161)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:301)
        at com.sun.proxy.$Proxy84.merge(Unknown Source)
        at org.springframework.data.jpa.repository.support.SimpleJpaRepository.save(SimpleJpaRepository.java:511)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.executeMethodOn(RepositoryFactorySupport.java:515)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.doInvoke(RepositoryFactorySupport.java:500)
        at org.springframework.data.repository.core.support.RepositoryFactorySupport$QueryExecutorMethodInterceptor.invoke(RepositoryFactorySupport.java:477)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.data.projection.DefaultMethodInvokingMethodInterceptor.invoke(DefaultMethodInvokingMethodInterceptor.java:56)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:99)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:136)
        ... 38 more
    

    上面是全部的异常信息,显示Unable to find com.sh.blog.entity.Child with id qfHfYhPxvwMEfadUFLLkuXwQGdUDsJCG,我们保存数据为啥她要去用idChild呢?神经病吧。于是一顿Google,看CascadeType的文档,看Hibernate的级联操作的文档,看……一下午过去了,一晚上过去了,一上午过去了。次日中午我决定,刨源码!

    到处打断点跟了很多次代码之后,我发现问题所在了。

    首先看repositorysave方法,我继承的是JpaRepository

    1. save方法
    @Transactional
    public <S extends T> S save(S entity) {
    
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
    

    咦?判断entity是不是新的?什么鬼,继续跟进isNew方法

    1. isNew方法
    public boolean isNew(T entity) {
    
        //取到ID值
        ID id = getId(entity);
        //取到ID字段的类
        Class<ID> idType = getIdType();
    
        //判断ID字段是不是原始类
        if (!idType.isPrimitive()) {
            return id == null;
        }
    
        //判断ID字段是否是Number的子类
        if (id instanceof Number) {
            return ((Number) id).longValue() == 0L;
        }
    
        //不支持的类型,抛异常
        throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
    }
    

    源码我已经注释了,看到这里我说一下她如何判断一个entity是不是新的。

    首先,判断entity的主键是不是原始类型(怎么判断我后面讲)。如果不是原始类型那就判断主键值,null就是新的,不为null就是旧的(我们暂且这么说);然后,如果主键是原始类型的话,看是不是Number的子类,也就是判断是不是数字,如果是就判断主键值是否等于0,0就是新的,不为0就是旧的;最后,抛异常,说咱不支持这类型~

    那她如何判断是否是原始类型呢?看源码

    1. isPrimitive方法
    /**
        * Determines if the specified {@code Class} object represents a
        * primitive type.
        *
        * <p> There are nine predefined {@code Class} objects to represent
        * the eight primitive types and void.  These are created by the Java
        * Virtual Machine, and have the same names as the primitive types that
        * they represent, namely {@code boolean}, {@code byte},
        * {@code char}, {@code short}, {@code int},
        * {@code long}, {@code float}, and {@code double}.
        *
        * <p> These objects may only be accessed via the following public static
        * final variables, and are the only {@code Class} objects for which
        * this method returns {@code true}.
        *
        * @return true if and only if this class represents a primitive type
        *
        * @see     java.lang.Boolean#TYPE
        * @see     java.lang.Character#TYPE
        * @see     java.lang.Byte#TYPE
        * @see     java.lang.Short#TYPE
        * @see     java.lang.Integer#TYPE
        * @see     java.lang.Long#TYPE
        * @see     java.lang.Float#TYPE
        * @see     java.lang.Double#TYPE
        * @see     java.lang.Void#TYPE
        * @since JDK1.1
        */
    public native boolean isPrimitive();
    

    明白了吧。上面注释说的这几种类型就是原始类型。

    搞清楚如何判断一个entity是否是新的,那我们回来看save方法的代码

    @Transactional
    public <S extends T> S save(S entity) {
    
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
    

    如果是entity是新的就用persist,否则就用merge。那按照上面说的方法,ParentChild的ID值是String,不是原始类型,然后我们又生成了一个随机字符串主键,那显然不是新的啊,走的是merge操作。靠!我级联PERSIST有毛用啊。那就换成MERGE

    @Getter
    @Setter
    @Entity
    public class Parent {
        @Id
        @Column(nullable = false, length = 32)
        private String id;
    
        @OneToMany(cascade = CascadeType.MERGE)
        @JoinColumn(name = "parentId")
        private List<Child> childList;
    }
    

    再次执行!


    success

    数据库的图片我就不贴了,正反是保存成功了。

    问题解决了,但开头为啥设置成CascadeType.PERSIST进行级联保存的时候报那样的错误呢?现在回头想想既然执行的是merge操作更新,那肯定是要先查一下数据库再更新啊,没有查到肯定报错了。

    总结

    如果你的数据表主键是String类型并且程序自己生成随机字符串填充,使用JpaRepositorysave方法保存数据,那CascadeType.PERSIST就不是级联保存了,而是“级联异常”了。需要换成CascadeType.MERGE,原因上面说了。

    但是转过头来想,如果主键依然是String类型,但不需要我们自己生成随机字符串填充,而是像自增主键那样把这项任务交出去,那我们的entity就是新的,就可以使用CascadeType.PERSIST保存了。例如像下面这样

    @Getter
    @Setter
    @Entity
    public class Parent {
        @Id
        @GeneratedValue(generator = "jpa-guid")
        @GenericGenerator(name = "jpa-guid", strategy = "guid")
        @Column(nullable = false, length = 36)
        private String id;
    
        //注意这里是PERSIST
        @OneToMany(cascade = CascadeType.PERSIST)
        @JoinColumn(name = "parentId")
        private List<Child> childList;
    }
    

    像上面这样写的话,就不用管ID生成了,像自增ID那样直接保存就行,ID会自动生成guid码填充(32位可装不下哦),也不用使用CascadeType.MERGE了,使用CascadeType.PERSIST级联保存即可(Child的主键生成策略也同时需要改)。

    题外话

    有些小伙伴可能看到了,我的Entity配置中写了@Getter@Setter注解,用过lombok组件的都知道,但有些小伙伴说了,你为啥不直接写成@Data呢?闲着没事儿吧?不是,我的意思是尽量不要赋予程序用不着的权限,也不要写程序用不着的方法。就像这个问题,如果一上手就写CascadeType.ALL,早就在家抱着媳妇儿喝咖啡了,但是如果写成CascadeType.ALL的话,程序有时可能就不会按照你的意志执行了,多了一些隐藏的bug,而这些bug导致的结果可能会让你抱着媳妇儿也寝食难安!

    相关文章

      网友评论

        本文标题:CascadeType.PERSIST 无法级联保存数据 源码级

        本文链接:https://www.haomeiwen.com/subject/lqyyqqtx.html