diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmJdbcExecutionContextAdapter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmJdbcExecutionContextAdapter.java index 4fce7252178f..dfde5b9fa904 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmJdbcExecutionContextAdapter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmJdbcExecutionContextAdapter.java @@ -70,4 +70,8 @@ public boolean hasQueryExecutionToBeAddedToStatistics() { return true; } + @Override + public boolean upgradeLocks() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/ExecutionContext.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/ExecutionContext.java index 8f30d4b7e591..248c779ff992 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/ExecutionContext.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/spi/ExecutionContext.java @@ -85,4 +85,12 @@ default boolean hasQueryExecutionToBeAddedToStatistics() { return false; } + /** + * Does this query return objects that might be already cached + * by the session, whose lock mode may need upgrading + */ + default boolean upgradeLocks(){ + return false; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/AbstractEntityInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/AbstractEntityInitializer.java index 20cd04f22ed0..353640348251 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/AbstractEntityInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/AbstractEntityInitializer.java @@ -555,7 +555,7 @@ && getEntityKey().getIdentifier().equals( executionContext.getEntityId() ) ) { } private void upgradeLockMode(RowProcessingState rowProcessingState) { - if ( lockMode != LockMode.NONE ) { + if ( lockMode != LockMode.NONE && rowProcessingState.upgradeLocks() ) { final EntityEntry entry = rowProcessingState.getSession().getPersistenceContextInternal() .getEntry( entityInstance ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowProcessingStateStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowProcessingStateStandardImpl.java index 2086ac4368f0..25e667dd2d79 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowProcessingStateStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/RowProcessingStateStandardImpl.java @@ -196,4 +196,9 @@ public Initializer resolveInitializer(NavigablePath path) { public boolean hasCollectionInitializers() { return this.initializers.hasCollectionInitializers(); } + + @Override + public boolean upgradeLocks() { + return executionContext.upgradeLocks(); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java new file mode 100644 index 000000000000..31d9b9fb14d0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java @@ -0,0 +1,96 @@ +package org.hibernate.orm.test.locking; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.stream.Stream; + +import org.hibernate.LockMode; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Version; + +@DomainModel(annotatedClasses = { + OptimisticAndPessimisticLockTest.EntityA.class +}) +@SessionFactory +@TestForIssue(jiraKey = "HHH-16461") +public class OptimisticAndPessimisticLockTest { + + public Stream pessimisticLockModes() { + return Stream.of(LockMode.UPGRADE_NOWAIT, LockMode.PESSIMISTIC_WRITE, LockMode.PESSIMISTIC_READ, LockMode.PESSIMISTIC_FORCE_INCREMENT); + } + + @ParameterizedTest + @MethodSource(value = "pessimisticLockModes") + public void upgradeFromOptimisticToPessimisticLock(LockMode pessimisticLockMode, SessionFactoryScope scope) { + Integer id = scope.fromTransaction( session -> { + EntityA entityA1 = new EntityA(); + entityA1.setPropertyA( 1 ); + session.persist( entityA1 ); + return entityA1.getId(); + } ); + scope.inTransaction( session -> { + EntityA entityA1 = session.find( EntityA.class, id ); + + // Do a concurrent change that will update the @Version property + scope.inTransaction( session2 -> { + var concurrentEntityA1 = session2.find( EntityA.class, id ); + concurrentEntityA1.setPropertyA( concurrentEntityA1.getPropertyA() + 1 ); + } ); + + // Refresh the entity with concurrent changes and upgrade the lock + session.refresh( entityA1, pessimisticLockMode ); + + entityA1.setPropertyA( entityA1.getPropertyA() * 2 ); + } ); + scope.inTransaction( session -> { + EntityA entityA1 = session.find( EntityA.class, id ); + assertThat( entityA1.getPropertyA() ).isEqualTo( ( 1 + 1 ) * 2 ); + } ); + } + + @Entity(name = "EntityA") + public static class EntityA { + + @Id + @GeneratedValue + Integer id; + + @Version + long version; + + int propertyA; + + public EntityA() { + } + + public EntityA(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public long getVersion() { + return version; + } + + public int getPropertyA() { + return propertyA; + } + + public void setPropertyA(int propertyA) { + this.propertyA = propertyA; + } + } +}