diff --git a/hibernate-core/src/main/java/org/hibernate/LockMode.java b/hibernate-core/src/main/java/org/hibernate/LockMode.java index 0d5dbf5aed6a..7e103adad675 100644 --- a/hibernate-core/src/main/java/org/hibernate/LockMode.java +++ b/hibernate-core/src/main/java/org/hibernate/LockMode.java @@ -52,7 +52,7 @@ public enum LockMode implements FindOption, RefreshOption { * rather than pull it from a cache. *

* This is the "default" lock mode, the mode requested by calling - * {@link Session#get(Class, Object)} without passing an explicit + * {@link Session#find(Class, Object)} without passing an explicit * mode. It permits the state of an object to be retrieved from * the cache without the cost of database access. * diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index d48ca8d3094e..cd5c02d0f6ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -825,19 +825,32 @@ public interface Session extends SharedSessionContract, EntityManager { void remove(Object object); /** - * Determine the current {@link LockMode} of the given managed instance associated - * with this session. + * Determine the current {@linkplain LockMode lock mode} held on the given + * managed instance associated with this session. + *

+ * Unlike the JPA-standard {@link #getLockMode}, this operation may be + * called when no transaction is active, in which case it should return + * {@link LockMode#NONE}, indicating that no pessimistic lock is held on + * the given entity. * * @param object a persistent instance associated with this session * - * @return the current lock mode + * @return the lock mode currently held on the given entity + * + * @throws IllegalStateException if the given instance is not associated + * with this persistence context + * @throws ObjectDeletedException if the given instance was already + * {@linkplain #remove removed} */ LockMode getCurrentLockMode(Object object); /** - * Completely clear the session. Evict all loaded instances and cancel all pending - * saves, updates and deletions. Do not close open iterators or instances of - * {@link ScrollableResults}. + * Completely clear the persistence context. Evict all loaded instances, + * causing every managed entity currently associated with this session to + * transition to the detached state, and cancel all pending insertions, + * updates, and deletions. + *

+ * Does not close open iterators or instances of {@link ScrollableResults}. */ @Override void clear(); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/ImmutableEntityEntry.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/ImmutableEntityEntry.java index e135e80bdc69..bfba4920a81b 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/ImmutableEntityEntry.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/ImmutableEntityEntry.java @@ -113,9 +113,10 @@ public static EntityEntry deserialize( (String) ois.readObject(), ois.readObject(), Status.valueOf( (String) ois.readObject() ), - ( previousStatusString = (String) ois.readObject() ).length() == 0 - ? null - : Status.valueOf( previousStatusString ), + ( previousStatusString = (String) ois.readObject() ) + .isEmpty() + ? null + : Status.valueOf( previousStatusString ), (Object[]) ois.readObject(), (Object[]) ois.readObject(), ois.readObject(), diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/ImmutableEntityEntryFactory.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/ImmutableEntityEntryFactory.java index 7dd1daaa0457..ca910513c509 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/ImmutableEntityEntryFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/ImmutableEntityEntryFactory.java @@ -18,7 +18,8 @@ * * @author Emmanuel Bernard */ -public class ImmutableEntityEntryFactory implements EntityEntryFactory { +@Deprecated(since = "7", forRemoval = true) +public final class ImmutableEntityEntryFactory implements EntityEntryFactory { /** * Singleton access */ diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/MutableEntityEntry.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/MutableEntityEntry.java index fba004857847..906dc2ed1e9f 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/MutableEntityEntry.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/MutableEntityEntry.java @@ -83,9 +83,10 @@ public static EntityEntry deserialize( (String) ois.readObject(), ois.readObject(), Status.valueOf( (String) ois.readObject() ), - ( previousStatusString = (String) ois.readObject() ).length() == 0 - ? null - : Status.valueOf( previousStatusString ), + ( previousStatusString = (String) ois.readObject() ) + .isEmpty() + ? null + : Status.valueOf( previousStatusString ), (Object[]) ois.readObject(), (Object[]) ois.readObject(), ois.readObject(), diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/MutableEntityEntryFactory.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/MutableEntityEntryFactory.java index 17caac436de5..41aa3e788eba 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/MutableEntityEntryFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/MutableEntityEntryFactory.java @@ -18,7 +18,8 @@ * * @author Emmanuel Bernard */ -public class MutableEntityEntryFactory implements EntityEntryFactory { +@Deprecated(since = "7", forRemoval = true) +public final class MutableEntityEntryFactory implements EntityEntryFactory { /** * Singleton access */ diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java index 4cea1e3fccae..f60e6d992097 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java @@ -655,58 +655,34 @@ public EntityEntry addEntry( final EntityPersister persister, final boolean disableVersionIncrement) { assert lockMode != null; - - final EntityEntry e; - - /* - IMPORTANT!!! - - The following instanceof checks and castings are intentional. - - DO NOT REFACTOR to make calls through the EntityEntryFactory interface, which would result - in polymorphic call sites which will severely impact performance. - - When a virtual method is called via an interface the JVM needs to resolve which concrete - implementation to call. This takes CPU cycles and is a performance penalty. It also prevents method - inlining which further degrades performance. Casting to an implementation and making a direct method call - removes the virtual call, and allows the methods to be inlined. In this critical code path, it has a very - large impact on performance to make virtual method calls. - */ - if ( persister.getEntityEntryFactory() instanceof MutableEntityEntryFactory ) { - //noinspection RedundantCast - e = ( (MutableEntityEntryFactory) persister.getEntityEntryFactory() ).createEntityEntry( - status, - loadedState, - rowId, - id, - version, - lockMode, - existsInDatabase, - persister, - disableVersionIncrement, - this - ); - } - else { - //noinspection RedundantCast - e = ( (ImmutableEntityEntryFactory) persister.getEntityEntryFactory() ).createEntityEntry( - status, - loadedState, - rowId, - id, - version, - lockMode, - existsInDatabase, - persister, - disableVersionIncrement, - this - ); - } - - entityEntryContext.addEntityEntry( entity, e ); - + final EntityEntry entityEntry = + persister.isMutable() + ? new MutableEntityEntry( + status, + loadedState, + rowId, + id, + version, + lockMode, + existsInDatabase, + persister, + disableVersionIncrement, + this + ) + : new ImmutableEntityEntry( + status, + loadedState, + rowId, + id, + version, + lockMode, + existsInDatabase, + persister, + disableVersionIncrement + ); + entityEntryContext.addEntityEntry( entity, entityEntry ); setHasNonReadOnlyEnties( status ); - return e; + return entityEntry; } @Override @@ -716,7 +692,6 @@ public EntityEntry addReferenceEntry( final EntityEntry entityEntry = asManagedEntity( entity ).$$_hibernate_getEntityEntry(); entityEntry.setStatus( status ); entityEntryContext.addEntityEntry( entity, entityEntry ); - setHasNonReadOnlyEnties( status ); return entityEntry; } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntryFactory.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntryFactory.java index d0bcc2e9d237..fc7b2420f922 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntryFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntryFactory.java @@ -13,7 +13,10 @@ * Contract to build {@link EntityEntry} * * @author Emmanuel Bernard + * + * @deprecated No longer used */ +@Deprecated(since = "7", forRemoval = true) public interface EntityEntryFactory extends Serializable { /** diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 141f18cd032c..5a6c26fdc701 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -525,9 +525,7 @@ private void checksBeforeQueryCreation() { public void prepareForQueryExecution(boolean requiresTxn) { checksBeforeQueryCreation(); if ( requiresTxn && !isTransactionInProgress() ) { - throw new TransactionRequiredException( - "Query requires transaction be in progress, but no transaction is known to be in progress" - ); + throw new TransactionRequiredException( "No active transaction" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index 9f54ecb571d1..02f25916ebf9 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -554,13 +554,13 @@ public LockMode getCurrentLockMode(Object object) { if ( e == null ) { throw new IllegalArgumentException( "Given entity is not associated with the persistence context" ); } - - if ( e.getStatus().isDeletedOrGone() ) { - throw new ObjectDeletedException( "The given object was deleted", e.getId(), + else if ( e.getStatus().isDeletedOrGone() ) { + throw new ObjectDeletedException( "Given entity was removed", e.getId(), e.getPersister().getEntityName() ); } - - return e.getLockMode(); + else { + return e.getLockMode(); + } } @Override @@ -2611,7 +2611,7 @@ private static CacheStoreMode determineCacheStoreMode(Map settin } private void checkTransactionNeededForUpdateOperation() { - checkTransactionNeededForUpdateOperation( "no transaction is in progress" ); + checkTransactionNeededForUpdateOperation( "No active transaction" ); } @Override @@ -2772,11 +2772,11 @@ public LockModeType getLockMode(Object entity) { checkOpen(); if ( !isTransactionInProgress() ) { - throw new TransactionRequiredException( "Call to EntityManager#getLockMode should occur within transaction according to spec" ); + throw new TransactionRequiredException( "No active transaction" ); } if ( !contains( entity ) ) { - throw getExceptionConverter().convert( new IllegalArgumentException( "entity not in the persistence context" ) ); + throw getExceptionConverter().convert( new IllegalArgumentException( "Entity not associated with the persistence context" ) ); } return LockModeTypeHelper.getLockModeType( getCurrentLockMode( entity ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index d2200705aa41..9ca947fd2226 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -4585,7 +4585,7 @@ private SQLQueryParser createSqlQueryParser(Table table) { @Override public EntityEntryFactory getEntityEntryFactory() { - return this.entityEntryFactory; + return entityEntryFactory; } /** diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java index 76e13f5e09f2..9479f0d42bc5 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java @@ -163,10 +163,11 @@ default String getSqlAliasStem() { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** - * Get the EntityEntryFactory indicated for the entity mapped by this persister. + * Get the {@link EntityEntryFactory} indicated for the entity mapped by this persister. * - * @return The proper EntityEntryFactory. + * @deprecated No longer used */ + @Deprecated(since = "7", forRemoval = true) EntityEntryFactory getEntityEntryFactory(); /** diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java index 03573f9fa780..17fd2006987c 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java @@ -87,7 +87,6 @@ import jakarta.persistence.ParameterMode; import jakarta.persistence.PersistenceException; import jakarta.persistence.TemporalType; -import jakarta.persistence.TransactionRequiredException; import jakarta.persistence.metamodel.Type; import static java.lang.Boolean.parseBoolean; @@ -823,10 +822,6 @@ protected ProcedureOutputs outputs() { @Override protected int doExecuteUpdate() { - if ( !getSession().isTransactionInProgress() ) { - throw new TransactionRequiredException( "jakarta.persistence.Query.executeUpdate requires active transaction" ); - } - // the expectation is that there is just one Output, of type UpdateCountOutput try { execute(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractQuery.java index e1b609b12f6a..1abb7ce170d4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractQuery.java @@ -8,7 +8,6 @@ import java.util.Calendar; import java.util.Collection; import java.util.Date; -import java.util.HashSet; import java.util.Map; import java.util.Set; @@ -634,8 +633,9 @@ protected void prepareForExecution() { @Override public int executeUpdate() throws HibernateException { - getSession().checkTransactionNeededForUpdateOperation( "Executing an update/delete query" ); - final HashSet fetchProfiles = beforeQueryHandlingFetchProfiles(); + //TODO: refactor copy/paste of QuerySqmImpl.executeUpdate() + getSession().checkTransactionNeededForUpdateOperation( "No active transaction for update or delete query" ); + final var fetchProfiles = beforeQueryHandlingFetchProfiles(); boolean success = false; try { final int result = doExecuteUpdate(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index 397e9d0f96be..05ec67530773 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -86,7 +86,6 @@ import java.util.Calendar; import java.util.Collection; import java.util.Date; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.function.BooleanSupplier; @@ -507,9 +506,10 @@ private QueryInterpretationCache interpretationCache() { @Override public int executeUpdate() { + //TODO: refactor copy/paste of AbstractQuery.executeUpdate() verifyUpdate(); - getSession().checkTransactionNeededForUpdateOperation( "Executing an update/delete query" ); - final HashSet fetchProfiles = beforeQueryHandlingFetchProfiles(); + getSession().checkTransactionNeededForUpdateOperation( "No active transaction for update or delete query" ); + final var fetchProfiles = beforeQueryHandlingFetchProfiles(); boolean success = false; try { final int result = doExecuteUpdate(); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/BaseExecutionContext.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/BaseExecutionContext.java index 0afdb826c0ad..7e4732944d01 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/BaseExecutionContext.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/BaseExecutionContext.java @@ -15,9 +15,11 @@ public class BaseExecutionContext implements ExecutionContext { private final SharedSessionContractImplementor session; + private final boolean transactionActive; public BaseExecutionContext(SharedSessionContractImplementor session) { this.session = session; + transactionActive = session.isTransactionInProgress(); } // Optimization: mark this as final so to avoid a megamorphic call on this @@ -27,6 +29,11 @@ public final SharedSessionContractImplementor getSession() { return session; } + @Override + public final boolean isTransactionActive() { + return transactionActive; + } + // Also marked as final for the same reason @Override public final LoadQueryInfluencers getLoadQueryInfluencers() { 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 a848e7352c46..c58dc56911db 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 @@ -25,6 +25,10 @@ default boolean isScrollResult(){ SharedSessionContractImplementor getSession(); + default boolean isTransactionActive() { + return getSession().isTransactionInProgress(); + } + QueryOptions getQueryOptions(); LoadQueryInfluencers getLoadQueryInfluencers(); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java index 00a1c02b1339..f08316e43636 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java @@ -1321,14 +1321,13 @@ protected void registerReloadedEntity(EntityInitializerData data) { @Override public void initializeInstance(EntityInitializerData data) { - if ( data.getState() != State.RESOLVED ) { - return; - } - if ( !skipInitialization( data ) ) { - assert consistentInstance( data ); - initializeEntityInstance( data ); + if ( data.getState() == State.RESOLVED ) { + if ( !skipInitialization( data ) ) { + assert consistentInstance( data ); + initializeEntityInstance( data ); + } + data.setState( State.INITIALIZED ); } - data.setState( State.INITIALIZED ); } protected boolean consistentInstance(EntityInitializerData data) { @@ -1375,7 +1374,14 @@ protected void initializeEntityInstance(EntityInitializerData data) { // from the perspective of Hibernate, an entity is read locked as soon as it is read // so regardless of the requested lock mode, we upgrade to at least the read level - final LockMode lockModeToAcquire = data.lockMode == LockMode.NONE ? LockMode.READ : data.lockMode; + final LockMode lockModeToAcquire; + if ( data.getRowProcessingState().isTransactionActive() ) { + lockModeToAcquire = data.lockMode == LockMode.NONE ? LockMode.READ : data.lockMode; + } + else { + // data read outside transaction is marked as unlocked + lockModeToAcquire = LockMode.NONE; + } final EntityEntry entityEntry = persistenceContext.addEntry( entityInstanceForNotify, diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/FindMultipleFromCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/FindMultipleFromCacheTest.java new file mode 100644 index 000000000000..af2cb21d6c47 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/loading/multiLoad/FindMultipleFromCacheTest.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.loading.multiLoad; + +import jakarta.persistence.Cacheable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.LockMode; +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.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@SessionFactory(generateStatistics = true) +@DomainModel(annotatedClasses = FindMultipleFromCacheTest.Record.class) +public class FindMultipleFromCacheTest { + @Test void test(SessionFactoryScope scope) { + scope.inStatelessTransaction(s-> { + s.insert(new Record(123L,"hello earth")); + s.insert(new Record(456L,"hello mars")); + }); + scope.inTransaction(s-> { + List all = s.findMultiple(Record.class, List.of(456L, 123L, 2L)); + Record mars = all.get( 0 ); + Record earth = all.get( 1 ); + assertEquals( LockMode.READ, s.getCurrentLockMode( mars ) ); + assertEquals( LockMode.READ, s.getCurrentLockMode( earth ) ); + assertEquals("hello mars", mars.message); + assertEquals("hello earth", earth.message); + assertNull(all.get(2)); + }); + assertEquals( 0, + scope.getSessionFactory().getStatistics().getSecondLevelCacheHitCount() ); + scope.getSessionFactory().getStatistics().clear(); + scope.inTransaction(s-> { + List all = s.findMultiple(Record.class, List.of(123L, 2L, 456L)); + Record earth = all.get( 0 ); + Record mars = all.get( 2 ); + assertEquals( LockMode.NONE, s.getCurrentLockMode( mars ) ); + assertEquals( LockMode.NONE, s.getCurrentLockMode( earth ) ); + assertEquals("hello earth", earth.message); + assertEquals("hello mars", mars.message); + assertNull(all.get(1)); + }); + assertEquals( 2, + scope.getSessionFactory().getStatistics().getSecondLevelCacheHitCount() ); + } + @Entity @Cacheable + static class Record { + @Id Long id; + String message; + + Record(Long id, String message) { + this.id = id; + this.message = message; + } + + Record() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/LockModeAcrossTransactionsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/LockModeAcrossTransactionsTest.java new file mode 100644 index 000000000000..7542a81e6417 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/LockModeAcrossTransactionsTest.java @@ -0,0 +1,107 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.locking; + +import jakarta.persistence.Cacheable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.LockMode; +import org.hibernate.ObjectDeletedException; +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.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@SessionFactory +@DomainModel(annotatedClasses = LockModeAcrossTransactionsTest.Cached.class) +class LockModeAcrossTransactionsTest { + + @Test void testWithEvict(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new Cached(5L) ); + } ); + scope.getSessionFactory().getCache().evict(Cached.class); + scope.inSession( session -> { + Cached cached = session.find( Cached.class, 5L ); + assertEquals( LockMode.NONE, session.getCurrentLockMode( cached ) ); + } ); + scope.getSessionFactory().getCache().evict(Cached.class); + scope.inTransaction( session -> { + Cached cached = session.find( Cached.class, 5L ); + assertEquals( LockMode.READ, session.getCurrentLockMode( cached ) ); + } ); + scope.getSessionFactory().getCache().evict(Cached.class); + scope.inSession( session -> { + Cached cached = session.createQuery( "from Cached", Cached.class ).getSingleResult(); + assertEquals( LockMode.NONE, session.getCurrentLockMode( cached ) ); + } ); + scope.getSessionFactory().getCache().evict(Cached.class); + scope.inTransaction( session -> { + Cached cached = session.createQuery( "from Cached", Cached.class ).getSingleResult(); + assertEquals( LockMode.READ, session.getCurrentLockMode( cached ) ); + } ); + scope.getSessionFactory().getCache().evict(Cached.class); + scope.inSession( session -> { + Cached cached = session.fromTransaction( tx -> { + Cached c = session.find( Cached.class, 5L ); + assertEquals( LockMode.READ, session.getCurrentLockMode( c ) ); + return c; + } ); + session.inTransaction( tx -> { + assertEquals( LockMode.NONE, session.getCurrentLockMode( cached ) ); + } ); + } ); + scope.inSession( session -> { + Cached cached = session.find( Cached.class, 5L ); + assertEquals( LockMode.NONE, session.getCurrentLockMode( cached ) ); + } ); + scope.inTransaction( session -> { + Cached cached = session.find( Cached.class, 5L ); + assertEquals( LockMode.NONE, session.getCurrentLockMode( cached ) ); + } ); + } + + @Test void testWithoutEvict(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Cached cached = new Cached( 3L ); + session.persist( cached ); + assertEquals( LockMode.WRITE, session.getCurrentLockMode( cached ) ); + } ); + scope.inSession( session -> { + Cached cached = session.find( Cached.class, 3L ); + assertEquals( LockMode.NONE, session.getCurrentLockMode( cached ) ); + } ); + scope.inTransaction( session -> { + Cached cached = session.find( Cached.class, 3L ); + assertEquals( LockMode.NONE, session.getCurrentLockMode( cached ) ); + cached.name = "Gavin"; + assertEquals( LockMode.NONE, session.getCurrentLockMode( cached ) ); + session.flush(); + assertEquals( LockMode.WRITE, session.getCurrentLockMode( cached ) ); + } ); + scope.inTransaction( session -> { + Cached cached = session.find( Cached.class, 3L ); + assertEquals( LockMode.NONE, session.getCurrentLockMode( cached ) ); + session.remove( cached ); + assertThrows( ObjectDeletedException.class, + () -> session.getCurrentLockMode( cached ) ); + } ); + } + + @Cacheable @Entity(name = "Cached") + static class Cached { + @Id + Long id; + String name; + Cached(Long id) { + this.id = id; + } + Cached() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/GetMultipleFromCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/GetMultipleFromCacheTest.java index 096d56490c30..f279fdab0248 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/GetMultipleFromCacheTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/stateless/GetMultipleFromCacheTest.java @@ -31,6 +31,8 @@ public class GetMultipleFromCacheTest { assertEquals("hello earth",all.get(1).message); assertNull(all.get(2)); }); + assertEquals( 0, + scope.getSessionFactory().getStatistics().getSecondLevelCacheHitCount() ); scope.getSessionFactory().getStatistics().clear(); scope.inStatelessTransaction(s-> { List all = s.getMultiple(Record.class, List.of(123L, 2L, 456L));