Mikhail Kadan
Mikhail Kadan

Reputation: 566

Hibernate / JPA use provided Ids for saving

Is there a simple to way to use provided Ids for entities when saving them into empty database?

When using EntityManager.persist(...) call it fails cause it thinks the entity is detached (as it has an Id). I don't want to use EntityManager.merge(...) for performance reasons (it fires an additional select for every entity).

I tried different ways, e.g. providing ForeignGenerator, but Ids are nulled then before saving and NPE is fired.

I also tried providing Hibernate Interceptor and overriding its isTransient() to always return true, but then I can't cascade save properly (or at least I have to maintain a cache of already persisted entities by myself).

I think there should be a simple way to do that, but can't find any.

Updated: added entity mappings

@Entity
@Data
@NoArgsConstructor
@EqualsAndHashCode(of = "admNr")
public class Adm implements Serializable {

    @Id
    @Column(name = "ADM_NR", nullable = false)
    private String admNr;

    @OneToMany(mappedBy = "adm", cascade = CascadeType.ALL)
    private Set<Kunde> kundeList = new HashSet<>();

}

@Entity
@Data
@NoArgsConstructor
@EqualsAndHashCode(of = "kundeNr")
public class Kunde implements Serializable {

    @Id
    @Column(name = "Kunde_Nr", nullable = false)
    private String kundeNr;

    @ManyToOne(optional = false)
    @JoinColumn(name = "ADM_Nr", referencedColumnName = "ADM_NR")
    private Adm adm;

}

Updated: provided additional info

I don't create entities manually, they are created as a part of data migration routine, and bidirectional relations are properly set in the process, so this is not possible that kunde.getAdm() will ever be null. Anyway, I tried removing nullable = false from Kunde.adm but the result was the same.

I did set up cascades to be able to save the entire graph using just em.persist(adm) as I didn't want to visit every child and persist it explicitly (the model I provided is a simplified one, actually there are more nesting levels in it).

String PKs are some legacy stuff I can't get rid of unfortunately.

The error I get when persisting the graph (calling em.persist(adm)) is:

java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation : de.apollon.dmx.workflow.processing.sqlite.adm.domain.Kunde.adm -> de.apollon.dmx.workflow.processing.sqlite.adm.domain.Adm
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:146) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:157) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:164) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:797) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:768) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
    at org.springframework.orm.jpa.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler.invoke(SharedEntityManagerCreator.java:305) ~[spring-orm-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at com.sun.proxy.$Proxy129.persist(Unknown Source) ~[na:na]
    at de.apollon.dmx.workflow.processing.sqlite.adm.service.SqliteAdmPackageService.storeAdmPackage(SqliteAdmPackageService.java:60) ~[classes/:na]
    at de.apollon.dmx.workflow.processing.sqlite.adm.service.SqliteAdmPackageService.createAdmPackageResource(SqliteAdmPackageService.java:48) ~[classes/:na]
    at de.apollon.dmx.workflow.processing.sqlite.adm.service.SqliteAdmPackageService$$FastClassBySpringCGLIB$$6a54d95.invoke(<generated>) ~[classes/:na]
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:746) ~[spring-aop-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:294) ~[spring-tx-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98) ~[spring-tx-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185) ~[spring-aop-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688) ~[spring-aop-5.0.8.RELEASE.jar:5.0.8.RELEASE]
    at de.apollon.dmx.workflow.processing.sqlite.adm.service.SqliteAdmPackageService$$EnhancerBySpringCGLIB$$5ba50508.createAdmPackageResource(<generated>) ~[classes/:na]
    at de.apollon.dmx.workflow.processing.service.AdmPackageService.createAdmPackage(AdmPackageService.java:59) ~[classes/:na]
    at de.apollon.dmx.workflow.processing.service.ProcessingService.processAdmPackage(ProcessingService.java:111) [classes/:na]
    at de.apollon.dmx.workflow.processing.service.ProcessingService.runTest(ProcessingService.java:58) [classes/:na]
    at de.apollon.dmx.workflow.Application.lambda$applicationRunner$0(Application.java:31) [classes/:na]
    at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:514) ~[na:na]
    at java.base/java.util.concurrent.FutureTask.runAndReset$$$capture(FutureTask.java:305) ~[na:na]
    at java.base/java.util.concurrent.FutureTask.runAndReset(FutureTask.java) ~[na:na]
    at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:305) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135) ~[na:na]
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
    at java.base/java.lang.Thread.run(Thread.java:844) ~[na:na]
Caused by: org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation : de.apollon.dmx.workflow.processing.sqlite.adm.domain.Kunde.adm -> de.apollon.dmx.workflow.processing.sqlite.adm.domain.Adm
    at org.hibernate.action.internal.UnresolvedEntityInsertActions.checkNoUnresolvedActionsAfterOperation(UnresolvedEntityInsertActions.java:122) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at org.hibernate.engine.spi.ActionQueue.checkNoUnresolvedActionsAfterOperation(ActionQueue.java:432) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at org.hibernate.internal.SessionImpl.checkNoUnresolvedActionsAfterOperation(SessionImpl.java:631) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:794) ~[hibernate-core-5.2.17.Final.jar:5.2.17.Final]
    ... 29 common frames omitted

Upvotes: 0

Views: 1205

Answers (2)

Mikhail Kadan
Mikhail Kadan

Reputation: 566

The problem was that the models were created with ModelMapper, and it provided different instances of the same entity at two sides of the relationship.

Adm (AdmNr="1234", Java instance @1234)
  .kundeList
  -> Kunde (KundeNr="2345", Java instance @2345)
    .adm
    -> Adm (AdmNr="1234", Java instance @4444) <- should be @1234

That lead to Hibernate thinking the Adm entity is transient and throwing an exception, as there is no cascade from Kunde to Adm.

Solved with adjusting ModelMapper so it provided the same instance for one PK.

Upvotes: 0

K.Nicholas
K.Nicholas

Reputation: 11561

You have a chicken and egg problem. One the one hand you have Adm with a Kunde Set and cascade = CascadeType.ALL. With cascade you are asking the Adm entity to save the Kunde relation for you without having to set the Adm field of the Kunde. So, I assume you are trying to save them both without doing so.

    Adm adm = new Adm();
    adm.setAdmNr("NO1");
    Kunde kunde = new Kunde();
    kunde.setKundeNr("NO1");
    adm.getKundeList().add(kunde);
    em.persist(adm);

On the other hand you have annotated the Kunde field with optional = false saying that it is impermissible to not set the Adm field of Kunde, which the above code doesn't do. Therefore it gives you a not-null error. You should have provided (part of) your stacktrace in your question.

Caused by: org.hibernate.PropertyValueException: not-null property references a null or transient value : model.Kunde.adm

So, to fix either remove the not null condition or set the Adm property of Kunde

    Adm adm = new Adm();
    adm.setAdmNr("NO1");
    Kunde kunde = new Kunde();
    kunde.setKundeNr("NO1");
    kunde.setAdm(adm);
    adm.getKundeList().add(kunde);
    em.persist(adm);

Also, as I said above, you don't need nullable = false on the primary keys because every primary key field is created with an unique index that prohibits nulls.

As a side note it is not a good idea to create a HashSet for the kundeList with every new Adm instance. You have a bidirectional mapping owned by the Kunde entity but that ownership is overridden by the cascade setting for a subset of operations. Assuming you are going to be doing more queries than updates the new HashSet will always be thrown out for the JPA managed list when you query. The cascade setting does not apply to queries so you won't be getting a filled out kundeList unless your query specifically asks for it and the Hashset will be replaced with an empty JPA managed list anyway. The above working code works fine if you simply add a em.persist(Kunde) to it and remove the cascade setting. For all these reasons and probably more I think it is preferable to manage the relationship specifically yourself and use the kundeList as a query only field for which it works best.

Finally, it's a really bad idea to use Strings as primary keys.

Upvotes: 1

Related Questions