Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JPA entities containing LAZY loaded relations cannot be merged in another transaction #38807

Closed
cghislai opened this issue Feb 16, 2024 · 9 comments · Fixed by #40230
Closed
Labels
area/hibernate-orm Hibernate ORM kind/bug Something isn't working
Milestone

Comments

@cghislai
Copy link

cghislai commented Feb 16, 2024

Describe the bug

If I have 3 entities A,B,C; a relation in B @JoinColumn(fetch = LAZY) A a and a relation in C @JoinColumn B b; then

If I fetch B in a JTA tranacstion, then merge it (calling EntityManager::merge) in another transaction, I get an error:

ERROR [io.qua.ver.htt.run.QuarkusErrorHandler] (executor-thread-1) HTTP Request to /hello/test failed, error id: 9ddaf11b-22a6-4e89-b324-733ef2c96b6f-1: org.hibernate.PropertyValueException: Detached entity with generated id '1' has an uninitialized version value 'null' : org.acme.entities.EntityA.version
	at org.hibernate.persister.entity.AbstractEntityPersister.isTransient(AbstractEntityPersister.java:4168)
	at org.hibernate.engine.internal.ForeignKeys.isTransient(ForeignKeys.java:293)
	at org.hibernate.type.EntityType.replace(EntityType.java:315)
	at org.hibernate.type.TypeHelper.replace(TypeHelper.java:88)
	at org.hibernate.event.internal.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:576)
	at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:427)
	at org.hibernate.event.internal.DefaultMergeEventListener.merge(DefaultMergeEventListener.java:208)

If I fetch C or A in a JTA transaction, then merge them in another one, it passes.

EDIT: The issue only occurs when the entity behind the lazy relation has a @Version field. In the reproducer, that would be EntityA#version.

The JPA spec states at https://jakarta.ee/specifications/persistence/3.0/jakarta-persistence-spec-3.0#merging-detached-entity-state:

The persistence provider must not merge fields marked LAZY that have not been fetched: it must ignore such fields when merging.

However here it seems quarkus is not ignoring the a field in A that is marked LAZY; and instead throws because the proxy does not contain the version value.

EDIT: spec has a paragraph about version field in that section as well. However, if I understand correctly it applies to the detached entity being merged, not the one behind the lazy relation.

Any Version columns used by the entity must be checked by the persistence runtime implementation during the merge operation and/or at flush or commit time. In the absence of Version columns there is no additional version checking done by the persistence provider runtime during the merge operation.

But If I merge a parent entity, like C, then it passes, and I can then merge B.

Expected behavior

I can fetch and merge JPA entities across tranasctions regarless of the columns FETCH type

Actual behavior

When I have an entity with a column maked with fetch type LAZY; I cannot merge it directly.

I would need to merge a parent entity to obtain a managed instance.

How to Reproduce?

The https://github.com/cghislai/quarkus-merge-lazy contains a reproducer test.

  • Notice how, with line 37 commented, I can merge entityB through its parent entityC.
  • Uncomment line 37 in org.acme.GreetingResource#hello to reproduce the error

Output of uname -a or ver

No response

Output of java -version

No response

Quarkus version or git rev

No response

Build tool (ie. output of mvnw --version or gradlew --version)

No response

Additional information

No response

@cghislai cghislai added the kind/bug Something isn't working label Feb 16, 2024
@geoand geoand added area/hibernate-orm Hibernate ORM and removed triage/needs-triage labels Feb 16, 2024
Copy link

quarkus-bot bot commented Feb 16, 2024

/cc @gsmet (hibernate-orm), @yrodiere (hibernate-orm)

@ymajoros
Copy link

I created a hibernate bug (though I'm not 100% sure it's not originating from Quarkus integration yet): https://hibernate.atlassian.net/browse/HHH-17750

@ymajoros
Copy link

Stack trace when GreetingResource:37 is uncommented in the reproducer:

	at org.hibernate.persister.entity.AbstractEntityPersister.isTransient(AbstractEntityPersister.java:4168)
	at org.hibernate.engine.internal.ForeignKeys.isTransient(ForeignKeys.java:293)
	at org.hibernate.type.EntityType.replace(EntityType.java:315)
	at org.hibernate.type.TypeHelper.replace(TypeHelper.java:88)
	at org.hibernate.event.internal.DefaultMergeEventListener.copyValues(DefaultMergeEventListener.java:576)
	at org.hibernate.event.internal.DefaultMergeEventListener.entityIsDetached(DefaultMergeEventListener.java:427)
	at org.hibernate.event.internal.DefaultMergeEventListener.merge(DefaultMergeEventListener.java:208)
	at org.hibernate.event.internal.DefaultMergeEventListener.doMerge(DefaultMergeEventListener.java:146)
	at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:126)
	at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:84)
	at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:127)
	at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:847)
	at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:833)
	at io.quarkus.hibernate.orm.runtime.session.TransactionScopedSession.merge(TransactionScopedSession.java:156)
	at org.hibernate.engine.spi.SessionLazyDelegator.merge(SessionLazyDelegator.java:271)
	at org.hibernate.Session_OpdLahisOZ9nWRPXMsEFQmQU03A_Synthetic_ClientProxy.merge(Unknown Source)
	at org.acme.services.ServiceB.doSomething(ServiceB.java:36)
	at org.acme.services.ServiceB_Subclass.doSomething$$superforward(Unknown Source)
	at org.acme.services.ServiceB_Subclass$$function$$4.apply(Unknown Source)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:73)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.proceed(AroundInvokeInvocationContext.java:62)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInOurTx(TransactionalInterceptorBase.java:136)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.invokeInOurTx(TransactionalInterceptorBase.java:107)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.doIntercept(TransactionalInterceptorRequired.java:38)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorBase.intercept(TransactionalInterceptorBase.java:61)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired.intercept(TransactionalInterceptorRequired.java:32)
	at io.quarkus.narayana.jta.runtime.interceptor.TransactionalInterceptorRequired_Bean.intercept(Unknown Source)
	at io.quarkus.arc.impl.InterceptorInvocation.invoke(InterceptorInvocation.java:42)
	at io.quarkus.arc.impl.AroundInvokeInvocationContext.perform(AroundInvokeInvocationContext.java:30)
	at io.quarkus.arc.impl.InvocationContexts.performAroundInvoke(InvocationContexts.java:27)
	at org.acme.services.ServiceB_Subclass.doSomething(Unknown Source)
	at org.acme.services.ServiceB_ClientProxy.doSomething(Unknown Source)
	at org.acme.GreetingResource.hello(GreetingResource.java:37)
	at org.acme.GreetingResource$quarkusrestinvoker$hello_e747664148511e1e5212d3e0f4b40d45c56ab8a1.invoke(Unknown Source)
	at org.jboss.resteasy.reactive.server.handlers.InvocationHandler.handle(InvocationHandler.java:29)
	at io.quarkus.resteasy.reactive.server.runtime.QuarkusResteasyReactiveRequestContext.invokeHandler(QuarkusResteasyReactiveRequestContext.java:141)
	at org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext.run(AbstractResteasyReactiveContext.java:147)
	at io.quarkus.vertx.core.runtime.VertxCoreRecorder$14.runWith(VertxCoreRecorder.java:582)
	at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2513)
	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1538)
	at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
	at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:1583)```

@cghislai
Copy link
Author

Note: We noticed this error only occurs when the entity behind the lazy relation has a @Version field. I edited original post accordingly. The reproducer will be updated as well.

@marko-bekhta
Copy link
Contributor

I created a hibernate bug (though I'm not 100% sure it's not originating from Quarkus integration yet): https://hibernate.atlassian.net/browse/HHH-17750

Thanks for taking a closer look. I have reproduced a similar error with just ORM; let's track the progress of this in the issue you've created in ORM JIRA.

@ymajoros
Copy link

@marko-bekhta I think this issue is typically mitigated in various frameworks:

  • Spring has "Open session in view".
  • Wildfly seems to have a similar mechanism by default (our application is migrating from Wildfly and the problem wasn't present).
  • Eclipselink (JPA RI) also allows to access lazy properties of detached entities by default (not even sure you can disable that).
  • enable_lazy_load_no_trans is mentioned in multiple sources, although I tried to activate it with quarkus.hibernate-orm.unsupported-properties."hibernate.enable_lazy_load_no_trans"=true, which is actually being read, but I couldn't find any usage besides configuration in Hibernate 6.4.

So I'm wondering: shouldn't Quarkus provide a similar mechanism?

@yrodiere
Copy link
Member

@ymajoros Feel free to open a feature request (or, more likely, link to an existing one), we can discuss that there.

Though I need to mention that "Open session in view" is widely considered as an anti-pattern, so the discussion (and eventual solution) will have to address that.

@ymajoros
Copy link

Good idea, I'll check out how to open a feature request if it's the way to go.

It's considered an antipattern by Vlad, which doesn't make it widely accepted. It's the default in Spring, Wildfly and equivalently in the reference implementation (Eclipselink does it by default). It's an endless discussion, I'm well aware of the pro's and con's, but truth is: if you have more than 1 transaction and lazy relations, you will end up with the need to fetch them outside of the first transaction. Projections our (which basically comes down to eager fetching) isn't always the right solution for that.

@yrodiere
Copy link
Member

Should be fixed by #40230

@yrodiere yrodiere added this to the 3.11 - main milestone Apr 24, 2024
@gsmet gsmet modified the milestones: 3.11 - main, 3.10.1, 3.8.5 May 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/hibernate-orm Hibernate ORM kind/bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants