前言
在業(yè)務(wù)開(kāi)發(fā)中泌绣,經(jīng)常遇到主鍵ID不能使用自增,而需要使用隨機(jī)字符串的情況预厌。但是在這種情況下阿迈,CascadeType.PERSIST
級(jí)聯(lián)保存就有問(wèn)題了。這里我假設(shè)大家知道幾種CascadeType是什么意思轧叽。話(huà)不多提苗沧,開(kāi)始探究
背景
Parent
表和Child
表, 單向一對(duì)多關(guān)系@OneToMany
目的
保存Parent
時(shí)級(jí)聯(lián)保存Child
Entity配置
- Parent
@Getter //lombok,下同
@Setter
@Entity
public class Parent {
@Id
@Column(nullable = false, length = 32)
private String id;
//默認(rèn)的配置項(xiàng)不再重復(fù)寫(xiě)
//例如OneToMany中的fetch默認(rèn)為FetchType.LAZY
//JoinColum中的referencedColumnName默認(rèn)為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;
}
兩個(gè)表的主鍵ID都使用了String
類(lèi)型炭晒。到此Entity寫(xiě)完了待逞,如果配置中的spring.jpa.hibernate.ddl-auto
你設(shè)置為update
或create
的話(huà)啟動(dòng)應(yīng)用之后數(shù)據(jù)庫(kù)中就有如下兩個(gè)表了
測(cè)試級(jí)聯(lián)保存
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));
//不用設(shè)置parentId哦
childList.add(child);
}
parent.setChildList(childList);
parentRepo.save(parent);
}
代碼很簡(jiǎn)單,不解釋了网严。重頭戲要來(lái)了识樱,運(yùn)行!
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
怜庸,我們保存數(shù)據(jù)為啥她要去用id
找Child
呢?神經(jīng)病吧垢村。于是一頓Google割疾,看CascadeType
的文檔,看Hibernate
的級(jí)聯(lián)操作的文檔嘉栓,看……一下午過(guò)去了杈曲,一晚上過(guò)去了驰凛,一上午過(guò)去了。次日中午我決定担扑,刨源碼!
到處打斷點(diǎn)跟了很多次代碼之后趣钱,我發(fā)現(xiàn)問(wèn)題所在了涌献。
首先看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
是不是新的?什么鬼井联,繼續(xù)跟進(jìn)isNew
方法
- isNew方法
public boolean isNew(T entity) {
//取到ID值
ID id = getId(entity);
//取到ID字段的類(lèi)
Class<ID> idType = getIdType();
//判斷ID字段是不是原始類(lèi)
if (!idType.isPrimitive()) {
return id == null;
}
//判斷ID字段是否是Number的子類(lèi)
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
//不支持的類(lèi)型卜壕,拋異常
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
}
源碼我已經(jīng)注釋了,看到這里我說(shuō)一下她如何判斷一個(gè)entity
是不是新的烙常。
首先轴捎,判斷entity
的主鍵是不是原始類(lèi)型(怎么判斷我后面講)。如果不是原始類(lèi)型那就判斷主鍵值蚕脏,null
就是新的侦副,不為null
就是舊的(我們暫且這么說(shuō));然后驼鞭,如果主鍵是原始類(lèi)型的話(huà)秦驯,看是不是Number的子類(lèi),也就是判斷是不是數(shù)字挣棕,如果是就判斷主鍵值是否等于0译隘,0就是新的,不為0就是舊的洛心;最后固耘,拋異常,說(shuō)咱不支持這類(lèi)型~
那她如何判斷是否是原始類(lèi)型呢皂甘?看源碼
- 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();
明白了吧玻驻。上面注釋說(shuō)的這幾種類(lèi)型就是原始類(lèi)型。
搞清楚如何判斷一個(gè)entity
是否是新的偿枕,那我們回來(lái)看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
。那按照上面說(shuō)的方法渐夸,Parent
和Child
的ID值是String
嗤锉,不是原始類(lèi)型,然后我們又生成了一個(gè)隨機(jī)字符串主鍵墓塌,那顯然不是新的啊瘟忱,走的是merge
操作奥额。靠访诱!我級(jí)聯(lián)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;
}
再次執(zhí)行!
數(shù)據(jù)庫(kù)的圖片我就不貼了触菜,正反是保存成功了九榔。
問(wèn)題解決了,但開(kāi)頭為啥設(shè)置成CascadeType.PERSIST
進(jìn)行級(jí)聯(lián)保存的時(shí)候報(bào)那樣的錯(cuò)誤呢涡相?現(xiàn)在回頭想想既然執(zhí)行的是merge
操作更新哲泊,那肯定是要先查一下數(shù)據(jù)庫(kù)再更新啊,沒(méi)有查到肯定報(bào)錯(cuò)了催蝗。
總結(jié)
如果你的數(shù)據(jù)表主鍵是String
類(lèi)型并且程序自己生成隨機(jī)字符串填充切威,使用JpaRepository
的save
方法保存數(shù)據(jù),那CascadeType.PERSIST
就不是級(jí)聯(lián)保存了丙号,而是“級(jí)聯(lián)異诚入”了。需要換成CascadeType.MERGE
槽袄,原因上面說(shuō)了烙无。
但是轉(zhuǎn)過(guò)頭來(lái)想,如果主鍵依然是String
類(lèi)型遍尺,但不需要我們自己生成隨機(jī)字符串填充截酷,而是像自增主鍵那樣把這項(xiàng)任務(wù)交出去,那我們的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;
}
像上面這樣寫(xiě)的話(huà),就不用管ID生成了鼓择,像自增ID那樣直接保存就行三幻,ID會(huì)自動(dòng)生成guid碼填充(32位可裝不下哦),也不用使用CascadeType.MERGE
了呐能,使用CascadeType.PERSIST
級(jí)聯(lián)保存即可(Child的主鍵生成策略也同時(shí)需要改)念搬。
題外話(huà)
有些小伙伴可能看到了,我的Entity配置中寫(xiě)了@Getter
和@Setter
注解摆出,用過(guò)lombok組件的都知道朗徊,但有些小伙伴說(shuō)了,你為啥不直接寫(xiě)成@Data
呢偎漫?閑著沒(méi)事兒吧爷恳?不是,我的意思是盡量不要賦予程序用不著的權(quán)限象踊,也不要寫(xiě)程序用不著的方法温亲。就像這個(gè)問(wèn)題棚壁,如果一上手就寫(xiě)CascadeType.ALL
,早就在家抱著媳婦兒喝咖啡了栈虚,但是如果寫(xiě)成CascadeType.ALL
的話(huà)袖外,程序有時(shí)可能就不會(huì)按照你的意志執(zhí)行了,多了一些隱藏的bug魂务,而這些bug導(dǎo)致的結(jié)果可能會(huì)讓你抱著媳婦兒也寢食難安在刺!