CascadeType.PERSIST can not concatenate to save data source level exploration

Preface

In business development, we often encounter the case that the primary key ID can not be self-increasing, but needs to use random strings. But in this case, CascadeType.PERSIST cascade storage is problematic. Here I assume that you know what several Cascade Types mean. Don't mention much. Start exploring.

background

Parent table and Child table, one-way one-to-many relationship @OneToMany

objective

Cascade save Child when saving Parent

Entity configuration

  • Parent
@Getter //lombok, the same below
@Setter
@Entity
public class Parent {

    @Id
    @Column(nullable = false, length = 32)
    private String id;

    //Default configuration items are no longer rewritten
    //For example, fetch in OneToMany defaults to FetchType.LAZY
    //ReferdColumnName in JoinColum defaults to the primary key of 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;
}

The primary key ID s of both tables use the String type. At this point Entity is finished. If you set spring.jpa.hibernate.ddl-auto in the configuration to update or create, you will start the application and then the database will have the following two tables

Test Cascade Save

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));
        //Don't set parentId
        childList.add(child);
    }

    parent.setChildList(childList);
    parentRepo.save(parent);
}

The code is simple and not explained. The big show is coming. Run it!! ____________

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

Above is all the exception information, showing Unable to find com.sh.blog.entity.Child with id qfHfYhPxvwMEfadUFLLkuXwQGdUDsJCG. Why does she use id to find Child when we save data? Neuropathy. So a meal of Google, look at Cascade Type documents, look at Hibernate cascade operations documents, look at... The afternoon passed, the night passed, and the morning passed. Next day at noon I decided to planer the source code!

After a lot of interruption points and code, I found the problem.

First, look at the save method of repository, which I inherited from JpaRepository.

  1. save method
@Transactional
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

Why? Is entity new? What the hell, keep up with the isNew method
2. isNew method

public boolean isNew(T entity) {

    //Get the ID value.
    ID id = getId(entity);
    //Class fetched to ID field
    Class<ID> idType = getIdType();

    //Determine whether the ID field is the original class
    if (!idType.isPrimitive()) {
        return id == null;
    }

    //Determine whether the ID field is a subclass of Number
    if (id instanceof Number) {
        return ((Number) id).longValue() == 0L;
    }

    //Unsupported types, throwing exceptions
    throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
}

I've already commented on the source code. See here, I'll talk about how she judges whether an entity is new or not.

First, determine whether the primary key of entity is the original type (how to judge what I will talk about later). If it is not the original type, then judge the primary key value, null is new, not null is old (let's say so for a moment); then, if the primary key is the original type, see if it is a subclass of Number, that is, judge whether it is a number, if it is equal to zero, zero is new, if not zero is new. Old; Finally, throw an exception and say we don't support this type.~

So how does she judge whether it's a primitive type? Look at source code
3. isPrimitive method

/**
    * 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();

You see. These types mentioned in the notes above are primitive types.

To figure out how to determine whether an entity is new, let's go back and see the code for the save method.

@Transactional
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

If entity is new, use persist, or merge. Then, according to the method mentioned above, the ID values of Parent and Child are String, not the original type, and then we generate a random string primary key, which is obviously not new, but the merge operation. Lean on! My cascade PERSIST is gross. That's MERGE instead.

@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;
}

Execute again!

I won't post the pictures in the database. The positive and negative are saved successfully.

The problem was solved, but why did you start by setting CascadeType.PERSIST to report such an error when cascading? Now think back, since the merge operation update is performed, it must first check the database and then update ah, did not find a positive error.

summary

If the primary key of your data table is String type and the program generates its own random string filling, use the save method of JpaRepository to save the data, then CascadeType.PERSIST is not a cascade save, but a "cascade exception". It needs to be replaced by CascadeType.MERGE for the reasons stated above.

But turning around, if the primary key is still String type, but we don't need to generate random string filling by ourselves, but hand over the task like the self-increasing primary key, then our entity is new and can be saved using CascadeType.PERSIST. For example, as follows

@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;

    //Note that this is PERSIST
    @OneToMany(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "parentId")
    private List<Child> childList;
}

If you write this above, you don't need to save IDs directly, just like self-increasing IDs. IDs will automatically generate guid code filling (32 bits can not be installed), and Cascade Type. MERGE will not be used. Cascade Type. PERSIST cascade can be used to save (Child's primary key generation strategy also needs to be changed).

Digression

Some of you may see that my Entity configuration has written @Getter and @Setter annotations, as you know from lombok components, but some of you said, why don't you write @Data directly? Are you free? No, I mean try not to give the program unnecessary permission, nor write the method that the program does not need. Just like this question, if you write CascadeType.ALL first, you'll have coffee with your daughter-in-law at home, but if you write CascadeType.ALL, sometimes the program won't execute according to your will. There are some hidden bugs, and the results of these bugs may make it difficult for you to sleep and eat with your daughter-in-law. Ann!

Tags: Java Hibernate Junit Database

Posted on Wed, 11 Sep 2019 19:57:39 -0700 by nitram