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));