前言
在业务开发中,经常遇到主键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
你设置为update
或create
的话启动应用之后数据库中就有如下两个表了
测试级联保存
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
,我们保存数据为啥她要去用id
找Child
呢?神经病吧。于是一顿Google,看CascadeType
的文档,看Hibernate
的级联操作的文档,看……一下午过去了,一晚上过去了,一上午过去了。次日中午我决定,刨源码!
到处打断点跟了很多次代码之后,我发现问题所在了。
首先看repository
的save
方法,我继承的是JpaRepository
。
- 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
。那按照上面说的方法,Parent
和Child
的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
进行级联保存的时候报那样的错误呢?现在回头想想既然执行的是merge
操作更新,那肯定是要先查一下数据库再更新啊,没有查到肯定报错了。
总结
如果你的数据表主键是String
类型并且程序自己生成随机字符串填充,使用JpaRepository
的save
方法保存数据,那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导致的结果可能会让你抱着媳妇儿也寝食难安!