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 find and locking a lazy referenced entity fails #39258

Open
mensinda opened this issue Mar 7, 2024 · 6 comments
Open

JPA find and locking a lazy referenced entity fails #39258

mensinda opened this issue Mar 7, 2024 · 6 comments
Labels
area/hibernate-orm Hibernate ORM kind/bug Something isn't working

Comments

@mensinda
Copy link

mensinda commented Mar 7, 2024

Describe the bug

Trying to find and lock (via EntityManager.find(*.class, ..., LockModeType.PESSIMISTIC_WRITE)) of an Entity fails if that entity:

  • Has a @Version attribute
  • Is referenced by another entity (main entity) with fetch = FetchType.LAZY
  • The referencing entity (main entity): is already loaded and locked
  • The referenced entity was not previously loaded in the same transaction

Abbreviated example from https://github.com/mensinda/quarkus-stuff/tree/lockLazyLock to illustrate:

@Entity
public class MainEntity {
    @Id
    private long id;

    @Version
    @Column(columnDefinition = "NUMERIC(9) DEFAULT 0")
    private long tanum;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "LEHRL")
    private ReferencedEntity referenced;

    protected MainEntity() {}

    public MainEntity(long id, ReferencedEntity referenced) {
        this.id = id;
        this.referenced = referenced;
    }
}

@Entity
public class ReferencedEntity {

    @Id
    private long id;

    @Version
    @Column(columnDefinition = "NUMERIC(9) DEFAULT 0")
    private long tanum;

    protected ReferencedEntity() {}

    public ReferencedEntity(long id) {
        this.id = id;
    }
}

@QuarkusTest
class DoTest {

    @Inject
    EntityManager em;

    @BeforeAll
    @Transactional
    static void setup() {
        final EntityManager em = CDI.current().select(EntityManager.class).get();
        final ReferencedEntity e1 = new ReferencedEntity(0);
        em.persist(e1);
        em.persist(new MainEntity(0, e1));
    }

    /**
     * FAILS!
     */
    @Test
    @Transactional
    void findAndLock() {
        MainEntity m = em.find(MainEntity.class, 0, LockModeType.PESSIMISTIC_WRITE);

        // This call will now throw an jakarta.persistence.OptimisticLockException
        ReferencedEntity e1 = em.find(ReferencedEntity.class, 0, LockModeType.PESSIMISTIC_WRITE);
    }
}

Expected behavior

No exceptions on the second em.find(..., LockModeType.PESSIMISTIC_WRITE);

Actual behavior

jakarta.persistence.OptimisticLockException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [root.ReferencedEntity#0]
	at org.hibernate.internal.ExceptionConverterImpl.wrapStaleStateException(ExceptionConverterImpl.java:209)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:95)
	at org.hibernate.internal.ExceptionConverterImpl.convert(ExceptionConverterImpl.java:184)
	at org.hibernate.internal.SessionImpl.find(SessionImpl.java:2470)
	at org.hibernate.internal.SessionImpl.find(SessionImpl.java:2400)
	at io.quarkus.hibernate.orm.runtime.session.TransactionScopedSession.find(TransactionScopedSession.java:191)
	at org.hibernate.engine.spi.SessionLazyDelegator.find(SessionLazyDelegator.java:829)
	at org.hibernate.Session_OpdLahisOZ9nWRPXMsEFQmQU03A_Synthetic_ClientProxy.find(Unknown Source)
	at test.DoTest.findAndLock(DoTest.java:97)
	at test.DoTest_Subclass.findAndLock$$superforward(Unknown Source)
	at test.DoTest_Subclass$$function$$1.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 test.DoTest_Subclass.findAndLock(Unknown Source)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at io.quarkus.test.junit.QuarkusTestExtension.runExtensionMethod(QuarkusTestExtension.java:1013)
	at io.quarkus.test.junit.QuarkusTestExtension.interceptTestMethod(QuarkusTestExtension.java:827)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	Suppressed: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [root.ReferencedEntity#0]
		at org.hibernate.dialect.lock.PessimisticWriteSelectLockingStrategy.lock(PessimisticWriteSelectLockingStrategy.java:81)
		at org.hibernate.persister.entity.AbstractEntityPersister.lock(AbstractEntityPersister.java:2189)
		at org.hibernate.loader.ast.internal.LoaderHelper.upgradeLock(LoaderHelper.java:93)
		at org.hibernate.loader.ast.internal.CacheEntityLoaderHelper.loadFromSessionCache(CacheEntityLoaderHelper.java:184)
		at org.hibernate.event.internal.DefaultLoadEventListener.doLoad(DefaultLoadEventListener.java:532)
		at org.hibernate.event.internal.DefaultLoadEventListener.load(DefaultLoadEventListener.java:207)
		at org.hibernate.event.internal.DefaultLoadEventListener.lockAndLoad(DefaultLoadEventListener.java:492)
		at org.hibernate.event.internal.DefaultLoadEventListener.doOnLoad(DefaultLoadEventListener.java:112)
		at org.hibernate.event.internal.DefaultLoadEventListener.onLoad(DefaultLoadEventListener.java:68)
		at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:138)
		at org.hibernate.internal.SessionImpl.fireLoadNoChecks(SessionImpl.java:1222)
		at org.hibernate.internal.SessionImpl.fireLoad(SessionImpl.java:1210)
		at org.hibernate.loader.internal.IdentifierLoadAccessImpl.load(IdentifierLoadAccessImpl.java:203)
		at org.hibernate.loader.internal.IdentifierLoadAccessImpl.doLoad(IdentifierLoadAccessImpl.java:160)
		at org.hibernate.loader.internal.IdentifierLoadAccessImpl.lambda$load$1(IdentifierLoadAccessImpl.java:149)
		at org.hibernate.loader.internal.IdentifierLoadAccessImpl.perform(IdentifierLoadAccessImpl.java:112)
		at org.hibernate.loader.internal.IdentifierLoadAccessImpl.load(IdentifierLoadAccessImpl.java:149)
		at org.hibernate.internal.SessionImpl.find(SessionImpl.java:2424)
		... 24 more
Caused by: [CIRCULAR REFERENCE: org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [root.ReferencedEntity#0]]

How to Reproduce?

I have created a minimal test case for this issue: https://github.com/mensinda/quarkus-stuff/tree/lockLazyLock

Steps to reproduce:

  1. clone / checkout the lockLazyLock branch of https://github.com/mensinda/quarkus-stuff/tree/lockLazyLock
  2. run mvn clean verify

Output of uname -a or ver

Linux XXXXXXX 5.15.0-56-generic #62-Ubuntu SMP Tue Nov 22 19:54:14 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

Output of java -version

openjdk version "17.0.10" 2024-01-16

Quarkus version or git rev

3.8.1

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

Apache Maven 3.9.2 (c9616018c7a021c1c39be70fb2843d6f5f9b8a1c)

Additional information

No response

@mensinda mensinda added the kind/bug Something isn't working label Mar 7, 2024
@mensinda
Copy link
Author

mensinda commented Mar 7, 2024

Also, I tried to reproduce this with just Hibernate alone, and it looks like the error does NOT occur there. So, I would assume that this really is a Quarkus bug this time around.


My hibernate test case:

package org.hibernate.orm.test.locking;

import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.LockModeType;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Version;
import org.hibernate.Hibernate;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.JiraKey;
import org.hibernate.testing.orm.junit.Jpa;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static org.junit.Assert.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@JiraKey("HHH-?????")
@Jpa(
		annotatedClasses = {
				LockFindAndLockReferencedTest.MainEntity.class,
				LockFindAndLockReferencedTest.ReferencedEntity.class
		}
)
public class LockFindAndLockReferencedTest {

	@BeforeAll
	public void setUp(EntityManagerFactoryScope scope) {
		scope.inTransaction(
				entityManager -> {
					final ReferencedEntity e1 = new ReferencedEntity( 0L );
					entityManager.persist( e1 );
					final MainEntity e3 = new MainEntity( 0L, e1 );
					entityManager.persist( e3 );
				}
		);
	}

	@Test
	public void testFindAndLockAfterLock(EntityManagerFactoryScope scope) {
		scope.inTransaction(
				entityManager -> {
                    // First find and lock the main entity
					MainEntity m = entityManager.find( MainEntity.class, 0L, LockModeType.PESSIMISTIC_WRITE );
					assertNotNull( m );
					ReferencedEntity lazyReference = m.referencedLazy();
					assertNotNull( lazyReference );
					assertFalse( Hibernate.isInitialized( lazyReference ) );

					// Then find and lock the referenced entity
                    ReferencedEntity lazyEntity = entityManager.find( ReferencedEntity.class, 0L, LockModeType.PESSIMISTIC_WRITE );
                    assertNotNull( lazyEntity );

					assertEquals( LockModeType.PESSIMISTIC_WRITE, entityManager.getLockMode( lazyEntity ) );
				} );
	}

	@Entity(name = "MainEntity2")
	public static class MainEntity {
		@Id
		private long id;

        @Version
        private long tanum;

		private String name;

		@ManyToOne(targetEntity = ReferencedEntity.class, fetch = FetchType.LAZY)
		@JoinColumn(name = "LAZY_COLUMN")
		private ReferencedEntity referencedLazy;

		protected MainEntity() {
		}

		public MainEntity(long id, ReferencedEntity lazy) {
			this.id = id;
			this.referencedLazy = lazy;
		}

		public ReferencedEntity referencedLazy() {
			return referencedLazy;
		}
	}

	@Entity(name = "ReferencedEntity2")
	public static class ReferencedEntity {

		@Id
		private long id;

        @Version
        private long tanum;

		protected ReferencedEntity() {
		}

		public ReferencedEntity(long id) {
			this.id = id;
		}
	}
}

@geoand geoand added area/hibernate-orm Hibernate ORM and removed triage/needs-triage labels Mar 8, 2024
@marko-bekhta
Copy link
Contributor

Hey @mensinda thanks for reporting this issue.

Quarkus applies a few settings that are different from the standalone Hibernate ORM configuration. Hence we've recently created this new test template to make it behave a bit closer to how Quarkus works: https://github.com/hibernate/hibernate-test-case-templates/blob/main/orm/hibernate-orm-6/src/test/java/org/hibernate/bugs/QuarkusLikeORMUnitTestCase.java

I've tried your case using that template and the issue is reproducable within it. Could you please give it a try and report the bug to the ORM JIRA here https://hibernate.atlassian.net/browse/HHH ? Thanks!

mensinda pushed a commit to mensinda/hibernate-orm that referenced this issue Mar 9, 2024
This test case demonstrates that currently find and locking an entity
that is already lazly loaded (by an already locked referencing entity) fails.

https://hibernate.atlassian.net/browse/HHH-17828
quarkusio/quarkus#39258
mensinda pushed a commit to mensinda/hibernate-orm that referenced this issue Mar 9, 2024
This test case demonstrates that currently find and locking an entity
that is already lazly loaded (by an already locked referencing entity) fails.

https://hibernate.atlassian.net/browse/HHH-17828
quarkusio/quarkus#39258
@mensinda
Copy link
Author

mensinda commented Mar 9, 2024

I have also managed to reproduce it with the BytecodeEnhancerRunner.

Upstream issue https://hibernate.atlassian.net/browse/HHH-17828
Reproducer PR hibernate/hibernate-orm#7961

@mensinda
Copy link
Author

mensinda commented Mar 9, 2024

@marko-bekhta the existence of that specific template should probably be documented somewhere. If there is already documentation, I couldn't find any.

@marko-bekhta
Copy link
Contributor

Thanks for creating the ticket and reproducer upstream!

the existence of that specific template should probably be documented somewhere

that's a good idea, I'm not sure where to add it though ... maybe @yrodiere would have a suggestion?

@yrodiere
Copy link
Member

yrodiere commented Mar 11, 2024

For Quarkus, not really, since the point of this reproducer is that it's useful only when a bug isn't actually about Quarkus.
I suppose there could be some guidance in the Quarkus issue template, but not sure we want anything specific to ORM there.

For ORM, adding stuff to the various readmes in the test case templates repo would be enough as far as I'm concerned.

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

No branches or pull requests

4 participants