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

前言

在业务开发中,经常遇到主键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的话启动应用之后数据库中就有如下两个表了
CascadeType.PERSIST 无法级联保存数据 源码级探究
CascadeType.PERSIST 无法级联保存数据 源码级探究

测试级联保存

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方法
2. 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就是旧的;最后,抛异常,说咱不支持这类型~

那她如何判断是否是原始类型呢?看源码
3. 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;
}

再次执行!
CascadeType.PERSIST 无法级联保存数据 源码级探究

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

问题解决了,但开头为啥设置成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导致的结果可能会让你抱着媳妇儿也寝食难安!

上一篇:MySQL 报错:Translating SQLException with SQL state '42000', error code '1064', mes


下一篇:java反射:入门、使用、原理