Reputation: 566
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
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
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