From 247f793ac6ba163c39f87cd964c5241a18b2a0c0 Mon Sep 17 00:00:00 2001 From: wanderer2097 Date: Mon, 30 Dec 2024 12:55:08 -0500 Subject: [PATCH 1/4] HHH-18992 adding tests --- .../multiLoad/MultiLoadLockingTest.java | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadLockingTest.java diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadLockingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadLockingTest.java new file mode 100644 index 000000000000..70bebf3ff13e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadLockingTest.java @@ -0,0 +1,235 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.loading.multiLoad; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.stream.Collectors; + +import org.hibernate.LockMode; +import org.hibernate.LockOptions; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.orm.test.cache.Company; +import org.hibernate.orm.test.cache.User; +import org.hibernate.testing.orm.domain.gambit.EntityWithAggregateId; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + + +@DomainModel( + annotatedClasses = { + MultiLoadLockingTest.Customer.class, + EntityWithAggregateId.class, + User.class, + Company.class + } + ) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true") + } +) +@JiraKey(value = "HHH-18992") +public class MultiLoadLockingTest { + + private List customerList = List.of( + new Customer(1L, "Customer A"), + new Customer(2L, "Customer B"), + new Customer(3L, "Customer C"), + new Customer(4L, "Customer D"), + new Customer(5L, "Customer E") + ); + + private List customerIds = customerList + .stream() + .map(Customer::getId) + .collect(Collectors.toList()); + + private List entityWithAggregateIdList = List.of( + new EntityWithAggregateId( new EntityWithAggregateId.Key( "1", "1" ), "Entity A" ), + new EntityWithAggregateId( new EntityWithAggregateId.Key( "2", "2" ), "Entity B" ), + new EntityWithAggregateId( new EntityWithAggregateId.Key( "3", "3" ), "Entity C" ), + new EntityWithAggregateId( new EntityWithAggregateId.Key( "4", "4" ), "Entity D" ), + new EntityWithAggregateId( new EntityWithAggregateId.Key( "5", "5" ), "Entity E" ) + ); + + private List entityWithAggregateIdKeys = entityWithAggregateIdList + .stream() + .map(EntityWithAggregateId::getKey) + .collect(Collectors.toList()); + + public List userList = List.of( + new User(1, null), + new User(2, null), + new User(3, null), + new User(4, null), + new User(5, null) + ); + + private List userIds = userList + .stream() + .map(User::getId) + .collect(Collectors.toList()); + + + @BeforeEach + public void prepareTestDataAndClearL2C(SessionFactoryScope scope) { + scope.inTransaction(session -> { + customerList.forEach( session::persist ); + entityWithAggregateIdList.forEach( session::persist ); + userList.forEach( session::persist ); + }); + scope.getSessionFactory().getCache().evictAll(); + } + + // (1) simple Id entity w/ pessimistic read lock + + @Test + void testMultiLoadSimpleIdEntityPessimisticReadLock(SessionFactoryScope scope) { + scope.inTransaction( session -> { + List customersLoaded = session.byMultipleIds(Customer.class) + .with(new LockOptions(LockMode.PESSIMISTIC_READ)) + .multiLoad(customerIds); + assertNotNull(customersLoaded); + assertEquals(customerList.size(), customersLoaded.size()); + customersLoaded.forEach(customer -> { + assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(customer)); + }); + } ); + } + + // (2) composite Id entity w/ pessimistic read lock (one of the entities already in L1C) + + @Test + void testMultiLoadCompositeIdEntityPessimisticReadLockAlreadyInSession( + SessionFactoryScope scope) { + scope.inTransaction( session -> { + EntityWithAggregateId entityInL1C = session + .find(EntityWithAggregateId.class, entityWithAggregateIdList.get(0).getKey()); + assertNotNull(entityInL1C); + List entitiesLoaded = session.byMultipleIds(EntityWithAggregateId.class) + .with(new LockOptions(LockMode.PESSIMISTIC_READ)) + .enableSessionCheck(true) + .multiLoad(entityWithAggregateIdKeys); + assertNotNull(entitiesLoaded); + assertEquals(entityWithAggregateIdList.size(), entitiesLoaded.size()); + entitiesLoaded.forEach(entity -> { + assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(entity)); + }); + } ); + } + + // (3) simple Id entity w/ pessimistic write lock (one in L1C & some in L2C) + + @Test + public void testMultiLoadSimpleIdEntityPessimisticWriteLockSomeInL1CAndSomeInL2C( + SessionFactoryScope scope) { + Integer userInL2CId = userIds.get(0); + Integer userInL1CId = userIds.get(1); + scope.inTransaction( session -> { + User userInL2C = session.find(User.class, userInL2CId); + assertNotNull(userInL2C); + } ); + scope.inTransaction( session -> { + assertTrue(session.getFactory().getCache().containsEntity(User.class, userInL2CId)); + User userInL1C = session.find(User.class, userInL1CId); + assertNotNull(userInL1C); + List usersLoaded = session.byMultipleIds(User.class) + .with(new LockOptions(LockMode.PESSIMISTIC_WRITE)) + .multiLoad(userIds); + assertNotNull(usersLoaded); + assertEquals(userList.size(), usersLoaded.size()); + usersLoaded.forEach(user -> { + assertEquals(LockMode.PESSIMISTIC_WRITE, session.getCurrentLockMode(user)); + }); + } ); + } + + + + // (4) simple Id entity w/ optimistic read lock + + @Test + void testMultiLoadSimpleIdEntityOptimisticReadLock(SessionFactoryScope scope) { + scope.inTransaction( session -> { + List customersLoaded = session.byMultipleIds(Customer.class) + .with(new LockOptions(LockMode.OPTIMISTIC)) + .multiLoad(customerIds); + assertNotNull(customersLoaded); + assertEquals(customerList.size(), customersLoaded.size()); + customersLoaded.forEach(customer -> { + assertEquals(LockMode.OPTIMISTIC, session.getCurrentLockMode(customer)); + }); + } ); + } + + + // (5) simple Id entity w/ optimistic force increment lock + + @Test + void testMultiLoadSimpleIdEntityOptimisticForceIncrementLock(SessionFactoryScope scope) { + scope.inTransaction( session -> { + List customersLoaded = session.byMultipleIds(Customer.class) + .with(new LockOptions(LockMode.OPTIMISTIC_FORCE_INCREMENT)) + .multiLoad(customerIds); + assertNotNull(customersLoaded); + assertEquals(customerList.size(), customersLoaded.size()); + customersLoaded.forEach(customer -> { + assertEquals(LockMode.OPTIMISTIC_FORCE_INCREMENT, session.getCurrentLockMode(customer)); + }); + } ); + } + + + + @Entity + public static class Customer { + + @Id + private Long id; + @Basic + private String name; + + protected Customer() { + } + + public Customer(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } +} From 1166181d5c8d65a494134ca5b4153b7a9ee7cee9 Mon Sep 17 00:00:00 2001 From: Jan Schatteman Date: Mon, 27 Jan 2025 22:55:02 +0100 Subject: [PATCH 2/4] HHH-18992 - a few changes/additions to the MultiLoadLockingTest - added an @AfterEach tearDown method - added tests also for findMultiple and byMultipleNaturalId - corrected the tests that use optimistic/optimistic_force_increment (they should use a versioned entity) - add statement inspection where applicable Signed-off-by: Jan Schatteman --- .../multiLoad/MultiLoadLockingTest.java | 444 +++++++++++++++--- 1 file changed, 369 insertions(+), 75 deletions(-) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadLockingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadLockingTest.java index 70bebf3ff13e..e17304f959f3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadLockingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/MultiLoadLockingTest.java @@ -4,25 +4,36 @@ */ package org.hibernate.orm.test.loading.multiLoad; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.Serializable; import java.util.List; -import java.util.stream.Collectors; +import java.util.function.Function; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Version; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.NaturalId; import org.hibernate.cfg.AvailableSettings; -import org.hibernate.orm.test.cache.Company; -import org.hibernate.orm.test.cache.User; -import org.hibernate.testing.orm.domain.gambit.EntityWithAggregateId; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.PostgreSQLSqlAstTranslator; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,12 +45,11 @@ @DomainModel( annotatedClasses = { MultiLoadLockingTest.Customer.class, - EntityWithAggregateId.class, - User.class, - Company.class + MultiLoadLockingTest.EntityWithAggregateId.class, + MultiLoadLockingTest.User.class } ) -@SessionFactory +@SessionFactory(useCollectingStatementInspector = true) @ServiceRegistry( settings = { @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), @@ -49,7 +59,9 @@ @JiraKey(value = "HHH-18992") public class MultiLoadLockingTest { - private List customerList = List.of( + private SQLStatementInspector sqlStatementInspector; + + private final List customerList = List.of( new Customer(1L, "Customer A"), new Customer(2L, "Customer B"), new Customer(3L, "Customer C"), @@ -57,12 +69,22 @@ public class MultiLoadLockingTest { new Customer(5L, "Customer E") ); - private List customerIds = customerList + private final List customerIdsAsLongs = customerList .stream() - .map(Customer::getId) - .collect(Collectors.toList()); + .map( Customer::getId ) + .toList(); + + private final List customerIdsAsObjects = customerList + .stream() + .map( (Function) Customer::getId ) + .toList(); + + private final List customerNaturalIdsAsObjects = customerList + .stream() + .map( (Function) Customer::getName ) + .toList(); - private List entityWithAggregateIdList = List.of( + private final List entityWithAggregateIdList = List.of( new EntityWithAggregateId( new EntityWithAggregateId.Key( "1", "1" ), "Entity A" ), new EntityWithAggregateId( new EntityWithAggregateId.Key( "2", "2" ), "Entity B" ), new EntityWithAggregateId( new EntityWithAggregateId.Key( "3", "3" ), "Entity C" ), @@ -70,141 +92,301 @@ public class MultiLoadLockingTest { new EntityWithAggregateId( new EntityWithAggregateId.Key( "5", "5" ), "Entity E" ) ); - private List entityWithAggregateIdKeys = entityWithAggregateIdList + private final List entityWithAggregateIdKeys = entityWithAggregateIdList .stream() .map(EntityWithAggregateId::getKey) - .collect(Collectors.toList()); - - public List userList = List.of( - new User(1, null), - new User(2, null), - new User(3, null), - new User(4, null), - new User(5, null) + .toList(); + + private final List entityWithAggregateIdKeysAsObjects = entityWithAggregateIdList + .stream() + .map( (Function) EntityWithAggregateId::getKey ) + .toList(); + + private final List entityWithAggregateIdNaturalIdsAsObjects = entityWithAggregateIdList + .stream() + .map( (Function) EntityWithAggregateId::getData ) + .toList(); + + public final List userList = List.of( + new User(1, "User 1"), + new User(2, "User 2"), + new User(3, "User 3"), + new User(4, "User 4"), + new User(5, "User 5") ); - private List userIds = userList + private final List userIds = userList .stream() .map(User::getId) - .collect(Collectors.toList()); + .toList(); + + private final List userIdsAsObjects = userList + .stream() + .map( (Function) User::getId ) + .toList(); + + private final List userNaturalIdsAsObjects = userList + .stream() + .map( (Function) User::getName ) + .toList(); @BeforeEach public void prepareTestDataAndClearL2C(SessionFactoryScope scope) { + sqlStatementInspector = scope.getCollectingStatementInspector(); + scope.inTransaction(session -> { customerList.forEach( session::persist ); entityWithAggregateIdList.forEach( session::persist ); userList.forEach( session::persist ); }); scope.getSessionFactory().getCache().evictAll(); + sqlStatementInspector.clear(); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> scope.getSessionFactory().getSchemaManager().truncate() ); + scope.getSessionFactory().getCache().evictAll(); } - // (1) simple Id entity w/ pessimistic read lock + // (1) simple Id entity w/ pessimistic read lock @Test void testMultiLoadSimpleIdEntityPessimisticReadLock(SessionFactoryScope scope) { + final LockOptions lockOptions = new LockOptions(LockMode.PESSIMISTIC_READ); + final String lockString = scope.getSessionFactory().getJdbcServices().getDialect().getForUpdateString(lockOptions); + + // test byMultipleIds scope.inTransaction( session -> { List customersLoaded = session.byMultipleIds(Customer.class) - .with(new LockOptions(LockMode.PESSIMISTIC_READ)) - .multiLoad(customerIds); + .with( lockOptions ) + .multiLoad(customerIdsAsLongs); assertNotNull(customersLoaded); assertEquals(customerList.size(), customersLoaded.size()); - customersLoaded.forEach(customer -> { - assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(customer)); - }); + customersLoaded.forEach(customer -> assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(customer)) ); + checkStatement( lockString ); + } ); + // test findMultiple + scope.inTransaction( session -> { + List customersLoaded = session.findMultiple(Customer.class, customerIdsAsObjects, LockMode.PESSIMISTIC_READ); + assertNotNull(customersLoaded); + assertEquals(customerList.size(), customersLoaded.size()); + customersLoaded.forEach(customer -> assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(customer)) ); + checkStatement( lockString ); + } ); + // test byMultipleNaturalId + scope.inTransaction( session -> { + List customersLoaded = session.byMultipleNaturalId(Customer.class) + .with( lockOptions ) + .multiLoad( customerNaturalIdsAsObjects ); + assertNotNull(customersLoaded); + assertEquals(customerList.size(), customersLoaded.size()); + customersLoaded.forEach(customer -> assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(customer)) ); + checkStatement( lockString ); } ); } // (2) composite Id entity w/ pessimistic read lock (one of the entities already in L1C) - @Test - void testMultiLoadCompositeIdEntityPessimisticReadLockAlreadyInSession( - SessionFactoryScope scope) { + void testMultiLoadCompositeIdEntityPessimisticReadLockAlreadyInSession(SessionFactoryScope scope) { + final LockOptions lockOptions = new LockOptions(LockMode.PESSIMISTIC_READ); + final String lockString = scope.getSessionFactory().getJdbcServices().getDialect().getForUpdateString(lockOptions); + scope.inTransaction( session -> { EntityWithAggregateId entityInL1C = session - .find(EntityWithAggregateId.class, entityWithAggregateIdList.get(0).getKey()); + .find(EntityWithAggregateId.class, entityWithAggregateIdList.get(0).getKey()); assertNotNull(entityInL1C); + sqlStatementInspector.clear(); + + // test byMultipleIds List entitiesLoaded = session.byMultipleIds(EntityWithAggregateId.class) - .with(new LockOptions(LockMode.PESSIMISTIC_READ)) - .enableSessionCheck(true) - .multiLoad(entityWithAggregateIdKeys); + .with( lockOptions ) + .multiLoad(entityWithAggregateIdKeys); + assertNotNull(entitiesLoaded); + assertEquals(entityWithAggregateIdList.size(), entitiesLoaded.size()); + entitiesLoaded.forEach(entity -> assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(entity)) ); + checkStatement( lockString ); + } ); + // test findMultiple + scope.inTransaction( session -> { + EntityWithAggregateId entityInL1C = session + .find(EntityWithAggregateId.class, entityWithAggregateIdList.get(0).getKey()); + assertNotNull(entityInL1C); + sqlStatementInspector.clear(); + + List entitiesLoaded = session.findMultiple(EntityWithAggregateId.class, + entityWithAggregateIdKeysAsObjects, LockMode.PESSIMISTIC_READ ); + assertNotNull(entitiesLoaded); + assertEquals(entityWithAggregateIdList.size(), entitiesLoaded.size()); + entitiesLoaded.forEach(entity -> assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(entity)) ); + checkStatement( lockString ); + } ); + // test byMultipleNaturalId + scope.inTransaction( session -> { + EntityWithAggregateId entityInL1C = session + .find(EntityWithAggregateId.class, entityWithAggregateIdList.get(0).getKey()); + assertNotNull(entityInL1C); + sqlStatementInspector.clear(); + + List entitiesLoaded = session.byMultipleNaturalId(EntityWithAggregateId.class) + .with( lockOptions ) + .multiLoad( entityWithAggregateIdNaturalIdsAsObjects ); assertNotNull(entitiesLoaded); assertEquals(entityWithAggregateIdList.size(), entitiesLoaded.size()); - entitiesLoaded.forEach(entity -> { - assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(entity)); - }); + entitiesLoaded.forEach(entity -> assertEquals(LockMode.PESSIMISTIC_READ, session.getCurrentLockMode(entity)) ); + checkStatement( lockString ); } ); } // (3) simple Id entity w/ pessimistic write lock (one in L1C & some in L2C) - @Test - public void testMultiLoadSimpleIdEntityPessimisticWriteLockSomeInL1CAndSomeInL2C( - SessionFactoryScope scope) { - Integer userInL2CId = userIds.get(0); - Integer userInL1CId = userIds.get(1); + public void testMultiLoadSimpleIdEntityPessimisticWriteLockSomeInL1CAndSomeInL2C(SessionFactoryScope scope) { + final Integer userInL2CId = userIds.get(0); + final Integer userInL1CId = userIds.get(1); + final LockOptions lockOptions = new LockOptions(LockMode.PESSIMISTIC_WRITE); + Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); + String lockString; + if ( PostgreSQLDialect.class.isAssignableFrom( dialect.getClass() ) ) { + PgSqlAstTranslatorExt translator = new PgSqlAstTranslatorExt( scope.getSessionFactory(), null ); + lockString = translator.getForUpdate(); + } + else { + lockString = scope.getSessionFactory().getJdbcServices().getDialect().getForUpdateString(lockOptions); + } + scope.inTransaction( session -> { User userInL2C = session.find(User.class, userInL2CId); assertNotNull(userInL2C); } ); + // test byMultipleIds scope.inTransaction( session -> { assertTrue(session.getFactory().getCache().containsEntity(User.class, userInL2CId)); User userInL1C = session.find(User.class, userInL1CId); assertNotNull(userInL1C); + sqlStatementInspector.clear(); + List usersLoaded = session.byMultipleIds(User.class) - .with(new LockOptions(LockMode.PESSIMISTIC_WRITE)) - .multiLoad(userIds); + .with( lockOptions ) + .multiLoad(userIds); assertNotNull(usersLoaded); assertEquals(userList.size(), usersLoaded.size()); - usersLoaded.forEach(user -> { - assertEquals(LockMode.PESSIMISTIC_WRITE, session.getCurrentLockMode(user)); - }); + usersLoaded.forEach(user -> assertEquals(LockMode.PESSIMISTIC_WRITE, session.getCurrentLockMode(user)) ); + checkStatement( lockString ); } ); - } + // test findMultiple + scope.inTransaction( session -> { + User userInL1C = session.find(User.class, userInL1CId); + assertNotNull(userInL1C); + sqlStatementInspector.clear(); + List usersLoaded = session.findMultiple(User.class, userIdsAsObjects, LockMode.PESSIMISTIC_WRITE); + assertNotNull(usersLoaded); + assertEquals(userList.size(), usersLoaded.size()); + usersLoaded.forEach(user -> assertEquals(LockMode.PESSIMISTIC_WRITE, session.getCurrentLockMode(user)) ); + checkStatement( lockString ); + } ); + // test byMultipleNaturalId + scope.inTransaction( session -> { + User userInL1C = session.find(User.class, userInL1CId); + assertNotNull(userInL1C); + sqlStatementInspector.clear(); + List usersLoaded = session.byMultipleNaturalId(User.class) + .with( lockOptions ) + .multiLoad( userNaturalIdsAsObjects ); + assertNotNull(usersLoaded); + assertEquals(userList.size(), usersLoaded.size()); + usersLoaded.forEach(user -> assertEquals(LockMode.PESSIMISTIC_WRITE, session.getCurrentLockMode(user)) ); + checkStatement( lockString ); + } ); + } // (4) simple Id entity w/ optimistic read lock - @Test void testMultiLoadSimpleIdEntityOptimisticReadLock(SessionFactoryScope scope) { + // test byMultipleIds scope.inTransaction( session -> { - List customersLoaded = session.byMultipleIds(Customer.class) - .with(new LockOptions(LockMode.OPTIMISTIC)) - .multiLoad(customerIds); - assertNotNull(customersLoaded); - assertEquals(customerList.size(), customersLoaded.size()); - customersLoaded.forEach(customer -> { - assertEquals(LockMode.OPTIMISTIC, session.getCurrentLockMode(customer)); - }); + List usersLoaded = session.byMultipleIds(User.class) + .with(new LockOptions(LockMode.OPTIMISTIC)) + .multiLoad(userIds); + assertNotNull(usersLoaded); + assertEquals(userList.size(), usersLoaded.size()); + usersLoaded.forEach(user -> assertEquals(LockMode.OPTIMISTIC, session.getCurrentLockMode(user)) ); + } ); + // test findMultiple + scope.inTransaction( session -> { + List usersLoaded = session.findMultiple(User.class, userIdsAsObjects, LockMode.OPTIMISTIC); + assertNotNull(usersLoaded); + assertEquals(userList.size(), usersLoaded.size()); + usersLoaded.forEach(user -> assertEquals(LockMode.OPTIMISTIC, session.getCurrentLockMode(user)) ); + } ); + // test byMultipleNaturalId + scope.inTransaction( session -> { + List usersLoaded = session.byMultipleNaturalId(User.class) + .with( new LockOptions(LockMode.OPTIMISTIC) ) + .multiLoad( userNaturalIdsAsObjects ); + assertNotNull(usersLoaded); + assertEquals(userList.size(), usersLoaded.size()); + usersLoaded.forEach(user -> assertEquals(LockMode.OPTIMISTIC, session.getCurrentLockMode(user)) ); } ); } // (5) simple Id entity w/ optimistic force increment lock - @Test void testMultiLoadSimpleIdEntityOptimisticForceIncrementLock(SessionFactoryScope scope) { + // test byMultipleIds scope.inTransaction( session -> { - List customersLoaded = session.byMultipleIds(Customer.class) - .with(new LockOptions(LockMode.OPTIMISTIC_FORCE_INCREMENT)) - .multiLoad(customerIds); - assertNotNull(customersLoaded); - assertEquals(customerList.size(), customersLoaded.size()); - customersLoaded.forEach(customer -> { - assertEquals(LockMode.OPTIMISTIC_FORCE_INCREMENT, session.getCurrentLockMode(customer)); - }); + List usersLoaded = session.byMultipleIds(User.class) + .with(new LockOptions(LockMode.OPTIMISTIC_FORCE_INCREMENT)) + .multiLoad(userIds); + assertNotNull(usersLoaded); + assertEquals(userList.size(), usersLoaded.size()); + usersLoaded.forEach(user -> assertEquals(LockMode.OPTIMISTIC_FORCE_INCREMENT, session.getCurrentLockMode(user)) ); + } ); + // test findMultiple + scope.inTransaction( session -> { + List usersLoaded = session.findMultiple(User.class, userIdsAsObjects, LockMode.OPTIMISTIC_FORCE_INCREMENT); + assertNotNull(usersLoaded); + assertEquals(userList.size(), usersLoaded.size()); + usersLoaded.forEach(user -> assertEquals(LockMode.OPTIMISTIC_FORCE_INCREMENT, session.getCurrentLockMode(user)) ); + } ); + // test byMultipleNaturalId + scope.inTransaction( session -> { + List usersLoaded = session.byMultipleNaturalId(User.class) + .with( new LockOptions(LockMode.OPTIMISTIC_FORCE_INCREMENT) ) + .multiLoad( userNaturalIdsAsObjects ); + assertNotNull(usersLoaded); + assertEquals(userList.size(), usersLoaded.size()); + usersLoaded.forEach(user -> assertEquals(LockMode.OPTIMISTIC_FORCE_INCREMENT, session.getCurrentLockMode(user)) ); } ); } + private void checkStatement(String lockString) { + assertEquals( 1,sqlStatementInspector.getSqlQueries().size() ); + assertTrue( sqlStatementInspector.getSqlQueries().get( 0 ).contains( lockString ) ); + sqlStatementInspector.clear(); + } + // Ugly-ish hack to be able to access the PostgreSQLSqlAstTranslator.getForUpdate() method needed for testing the PostgreSQL dialects + private static class PgSqlAstTranslatorExt extends PostgreSQLSqlAstTranslator { + public PgSqlAstTranslatorExt(SessionFactoryImplementor sessionFactory, Statement statement) { + super( sessionFactory, statement ); + } - @Entity - public static class Customer { + @Override + protected String getForUpdate() { + return super.getForUpdate(); + } + } + @Entity(name = "Customer") + public static class Customer { @Id private Long id; - @Basic + @Basic(optional = false) + @NaturalId private String name; protected Customer() { @@ -230,6 +412,118 @@ public String getName() { public void setName(String name) { this.name = name; } + } + + @Entity(name = "EntityWithAggregateId") + public static class EntityWithAggregateId { + @EmbeddedId + private EntityWithAggregateId.Key key; + @NaturalId + private String data; + + public EntityWithAggregateId() { + } + + public EntityWithAggregateId(EntityWithAggregateId.Key key, String data) { + this.key = key; + this.data = data; + } + + public EntityWithAggregateId.Key getKey() { + return key; + } + + public void setKey(EntityWithAggregateId.Key key) { + this.key = key; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + + @Embeddable + public static class Key implements Serializable { + private String value1; + private String value2; + + public Key() { + } + + public Key(String value1, String value2) { + this.value1 = value1; + this.value2 = value2; + } + + public String getValue1() { + return value1; + } + + public void setValue1(String value1) { + this.value1 = value1; + } + + public String getValue2() { + return value2; + } + + public void setValue2(String value2) { + this.value2 = value2; + } + } + } + + @Entity(name = "MyUser") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class User { + @Id + int id; + + @Version + private int version; + + @NaturalId + private String name; + + public User() { + } + + public User(int id) { + this.id = id; + } + + public User(int id, String name) { + this.id = id; + this.name = name; + } + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } + } From 78582d73f0e252fdf48c9205ef7e10661f99dcef Mon Sep 17 00:00:00 2001 From: Jan Schatteman Date: Thu, 30 Jan 2025 22:59:22 +0100 Subject: [PATCH 3/4] HHH-18992 - set NaturalIdMultiLoadAccessStandard.orderedReturnEnabled by default to false, as otherwise an UnsupportedOperationException would be thrown by the MultiNaturalIdLoaderInPredicate Signed-off-by: Jan Schatteman --- .../hibernate/internal/NaturalIdMultiLoadAccessStandard.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java index 55c98ecb1ed6..07e8af8500df 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NaturalIdMultiLoadAccessStandard.java @@ -34,7 +34,7 @@ public class NaturalIdMultiLoadAccessStandard implements NaturalIdMultiLoadAc private Integer batchSize; private boolean returnOfDeletedEntitiesEnabled; - private boolean orderedReturnEnabled = true; + private boolean orderedReturnEnabled = false; public NaturalIdMultiLoadAccessStandard(EntityPersister entityDescriptor, SessionImpl session) { this.entityDescriptor = entityDescriptor; From 8e575c48d6180795fb349cf77fc072ae4f906f67 Mon Sep 17 00:00:00 2001 From: Jan Schatteman Date: Tue, 28 Jan 2025 23:18:05 +0100 Subject: [PATCH 4/4] HHH-18992 - fix for issue (addition to fix provided in https://github.com/hibernate/hibernate-orm/pull/9517) Signed-off-by: Jan Schatteman --- ...utionContextWithSubselectFetchHandler.java | 23 ++++++++++++++++--- .../MultiIdEntityLoaderArrayParam.java | 3 ++- .../internal/MultiIdEntityLoaderStandard.java | 3 ++- .../MultiNaturalIdLoadingBatcher.java | 5 +++- .../internal/SingleIdExecutionContext.java | 4 ++++ 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/ExecutionContextWithSubselectFetchHandler.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/ExecutionContextWithSubselectFetchHandler.java index e05e929b8662..d0acc63d0e13 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/ExecutionContextWithSubselectFetchHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/ExecutionContextWithSubselectFetchHandler.java @@ -4,9 +4,11 @@ */ package org.hibernate.loader.ast.internal; +import org.hibernate.LockOptions; import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SubselectFetch; +import org.hibernate.query.internal.SimpleQueryOptions; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.exec.internal.BaseExecutionContext; @@ -14,20 +16,30 @@ class ExecutionContextWithSubselectFetchHandler extends BaseExecutionContext { private final SubselectFetch.RegistrationHandler subSelectFetchableKeysHandler; private final boolean readOnly; + private final QueryOptions queryOptions; public ExecutionContextWithSubselectFetchHandler( SharedSessionContractImplementor session, SubselectFetch.RegistrationHandler subSelectFetchableKeysHandler) { - this( session, subSelectFetchableKeysHandler, false ); + super( session ); + this.subSelectFetchableKeysHandler = subSelectFetchableKeysHandler; + this.readOnly = false; + this.queryOptions = QueryOptions.NONE; } public ExecutionContextWithSubselectFetchHandler( SharedSessionContractImplementor session, SubselectFetch.RegistrationHandler subSelectFetchableKeysHandler, - boolean readOnly) { + boolean readOnly, + LockOptions lockOptions) { super( session ); this.subSelectFetchableKeysHandler = subSelectFetchableKeysHandler; this.readOnly = readOnly; + this.queryOptions = determineQueryOptions( readOnly, lockOptions ); + } + + private QueryOptions determineQueryOptions(boolean readOnly, LockOptions lockOptions) { + return new SimpleQueryOptions( lockOptions, readOnly ? true : null ); } @Override @@ -39,6 +51,11 @@ public void registerLoadingEntityHolder(EntityHolder holder) { @Override public QueryOptions getQueryOptions() { - return readOnly ? QueryOptions.READ_ONLY : super.getQueryOptions(); + return queryOptions; + } + + @Override + public boolean upgradeLocks() { + return true; } } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java index 1c8ab3effbbc..9567cf816350 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java @@ -142,7 +142,8 @@ public LockOptions getLockOptions() { JdbcParametersList.singleton( jdbcParameter ), jdbcParameterBindings ), - TRUE.equals( loadOptions.getReadOnly( session ) ) + TRUE.equals( loadOptions.getReadOnly( session ) ), + lockOptions ), RowTransformerStandardImpl.instance(), null, diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderStandard.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderStandard.java index b5a9a2ef33d7..62b052e424cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderStandard.java @@ -173,7 +173,8 @@ public LockOptions getLockOptions() { new ExecutionContextWithSubselectFetchHandler( session, fetchableKeysHandler( session, sqlAst, jdbcParameters, jdbcParameterBindings ), - TRUE.equals( loadOptions.getReadOnly( session ) ) + TRUE.equals( loadOptions.getReadOnly( session ) ), + lockOptions ), RowTransformerStandardImpl.instance(), null, diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java index 8a74761c8219..16fb16a63dfb 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiNaturalIdLoadingBatcher.java @@ -51,6 +51,8 @@ interface KeyValueResolver { private final JdbcOperationQuerySelect jdbcSelect; + private final LockOptions lockOptions; + public MultiNaturalIdLoadingBatcher( EntityMappingType entityDescriptor, ModelPart restrictedPart, @@ -88,6 +90,7 @@ public LockOptions getLockOptions() { return lockOptions; } } ); + this.lockOptions = lockOptions; } public List multiLoad(Object[] naturalIdValues, SharedSessionContractImplementor session) { @@ -163,7 +166,7 @@ private List performLoad( return session.getJdbcServices().getJdbcSelectExecutor().list( jdbcSelect, jdbcParamBindings, - new ExecutionContextWithSubselectFetchHandler( session, subSelectFetchableKeysHandler ), + new ExecutionContextWithSubselectFetchHandler( session, subSelectFetchableKeysHandler, false, lockOptions ), RowTransformerStandardImpl.instance(), null, ListResultsConsumer.UniqueSemantic.FILTER, diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdExecutionContext.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdExecutionContext.java index 96e05fd0fff5..f0635c77957d 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdExecutionContext.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SingleIdExecutionContext.java @@ -76,4 +76,8 @@ public void registerLoadingEntityHolder(EntityHolder holder) { subSelectFetchableKeysHandler.addKey( holder ); } + @Override + public boolean upgradeLocks() { + return true; + } }