HHH-18001 introduce support for FindOptions

Signed-off-by: Gavin King <gavin@hibernate.org>
This commit is contained in:
Gavin King 2024-04-27 23:19:41 +02:00 committed by Steve Ebersole
parent 23b461a109
commit be6d8a9917
8 changed files with 251 additions and 9 deletions

View File

@ -9,6 +9,7 @@ package org.hibernate;
import java.util.Locale;
import jakarta.persistence.CacheRetrieveMode;
import jakarta.persistence.CacheStoreMode;
import jakarta.persistence.FindOption;
/**
* Controls how the session interacts with the {@linkplain Cache second-level cache}
@ -30,7 +31,7 @@ import jakarta.persistence.CacheStoreMode;
* @see CacheStoreMode
* @see CacheRetrieveMode
*/
public enum CacheMode {
public enum CacheMode implements FindOption {
/**
* The session may read items from the cache, and add items to the cache

View File

@ -0,0 +1,26 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate;
import jakarta.persistence.FindOption;
/**
* A {@link jakarta.persistence.FindOption} which requests a named
* {@linkplain org.hibernate.annotations.FetchProfile fetch profile}.
*
* @param profileName the {@link org.hibernate.annotations.FetchProfile#name()}
*
* @since 7.0
*
* @see org.hibernate.annotations.FetchProfile
* @see Session#find(Class, Object, FindOption...)
*
* @author Gavin King
*/
public record EnabledFetchProfile(String profileName)
implements FindOption {
}

View File

@ -6,6 +6,7 @@
*/
package org.hibernate;
import jakarta.persistence.FindOption;
import org.hibernate.jpa.internal.util.LockModeTypeHelper;
import jakarta.persistence.LockModeType;
@ -42,7 +43,7 @@ import java.util.Locale;
* @see LockOptions
* @see org.hibernate.annotations.OptimisticLocking
*/
public enum LockMode {
public enum LockMode implements FindOption {
/**
* No lock required. If an object is requested with this lock
* mode, a {@link #READ} lock will be obtained if it turns out

View File

@ -14,6 +14,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import jakarta.persistence.FindOption;
import jakarta.persistence.PessimisticLockScope;
import org.hibernate.query.Query;
import org.hibernate.query.spi.QueryOptions;
@ -51,7 +52,7 @@ import static java.util.Collections.unmodifiableSet;
*
* @author Scott Marlow
*/
public class LockOptions implements Serializable {
public class LockOptions implements FindOption, Serializable {
/**
* Represents {@link LockMode#NONE}, to which timeout and scope are
* not applicable.

View File

@ -0,0 +1,38 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate;
import jakarta.persistence.FindOption;
/**
* A {@link jakarta.persistence.FindOption} which requests that
* entities be loaded in {@link #READ_ONLY} mode or in regular
* {@link #READ_WRITE} mode.
*
* @since 7.0
*
* @see Session#setDefaultReadOnly(boolean)
* @see Session#find(Class, Object, FindOption...)
*
* @author Gavin King
*/
public enum ReadOnlyMode implements FindOption {
/**
* Specifies that an entity should be loaded in read-only mode.
* <p>
* Read-only entities are not dirty-checked and snapshots of
* persistent state are not maintained. Read-only entities can
* be modified, but changes are not persisted.
*/
READ_ONLY,
/**
* Specifies that an entity should be loaded in the default
* modifiable mode, regardless of the default mode of the
* session.
*/
READ_WRITE
}

View File

@ -407,8 +407,8 @@ public interface Session extends SharedSessionContract, EntityManager {
* changes are not persisted.
* <p>
* When a proxy is initialized, the loaded entity will have the same
* read-only/modifiable setting as the uninitialized
* proxy has, regardless of the session's current setting.
* read-only/modifiable setting as the uninitialized proxy has,
* regardless of the session's current setting.
* <p>
* To change the read-only/modifiable setting for a particular entity
* or proxy that already belongs to this session use

View File

@ -22,9 +22,12 @@ import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import jakarta.persistence.PessimisticLockScope;
import jakarta.persistence.Timeout;
import org.hibernate.CacheMode;
import org.hibernate.ConnectionAcquisitionMode;
import org.hibernate.EntityFilterException;
import org.hibernate.EnabledFetchProfile;
import org.hibernate.FetchNotFoundException;
import org.hibernate.FlushMode;
import org.hibernate.HibernateException;
@ -39,6 +42,7 @@ import org.hibernate.NaturalIdLoadAccess;
import org.hibernate.NaturalIdMultiLoadAccess;
import org.hibernate.ObjectDeletedException;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.ReadOnlyMode;
import org.hibernate.ReplicationMode;
import org.hibernate.Session;
import org.hibernate.SessionEventListener;
@ -106,6 +110,7 @@ import org.hibernate.event.spi.ResolveNaturalIdEventListener;
import org.hibernate.event.spi.SaveOrUpdateEvent;
import org.hibernate.event.spi.SaveOrUpdateEventListener;
import org.hibernate.graph.GraphSemantic;
import org.hibernate.graph.RootGraph;
import org.hibernate.graph.spi.RootGraphImplementor;
import org.hibernate.internal.util.ExceptionHelper;
import org.hibernate.jpa.internal.LegacySpecHelper;
@ -2557,16 +2562,58 @@ public class SessionImpl
}
}
private <T> IdentifierLoadAccessImpl<T> loadAccessWithOptions(Class<T> entityClass, FindOption[] options) {
final IdentifierLoadAccessImpl<T> loadAccess = byId(entityClass);
CacheStoreMode storeMode = null;
CacheRetrieveMode retrieveMode = null;
LockOptions lockOptions = new LockOptions();
for ( FindOption option : options) {
if ( option instanceof CacheStoreMode ) {
storeMode = (CacheStoreMode) option;
}
else if ( option instanceof CacheRetrieveMode ) {
retrieveMode = (CacheRetrieveMode) option;
}
else if ( option instanceof LockModeType ) {
lockOptions.setLockMode( LockModeTypeHelper.getLockMode( (LockModeType) option ) );
}
else if ( option instanceof LockMode ) {
lockOptions.setLockMode( (LockMode) option );
}
else if ( option instanceof LockOptions ) {
lockOptions = (LockOptions) option;
}
else if ( option instanceof PessimisticLockScope ) {
lockOptions.setLockScope( (PessimisticLockScope) option );
}
else if ( option instanceof Timeout ) {
final Timeout timeout = (Timeout) option;
lockOptions.setTimeOut( timeout.milliseconds() );
}
else if ( option instanceof EnabledFetchProfile ) {
EnabledFetchProfile enabledFetchProfile = (EnabledFetchProfile) option;
loadAccess.enableFetchProfile( enabledFetchProfile.profileName() );
}
else if ( option instanceof ReadOnlyMode ) {
loadAccess.withReadOnly( option == ReadOnlyMode.READ_ONLY );
}
}
loadAccess.with( lockOptions ).with( interpretCacheMode( storeMode, retrieveMode ) );
return loadAccess;
}
@Override
public <T> T find(Class<T> entityClass, Object primaryKey, FindOption... options) {
// todo (jpa 3.2) : implement
throw new UnsupportedOperationException( "Not yet implemented" );
return loadAccessWithOptions( entityClass, options ).load( primaryKey );
}
@Override
public <T> T find(EntityGraph<T> entityGraph, Object primaryKey, FindOption... options) {
// todo (jpa 3.2) : implement
throw new UnsupportedOperationException( "Not yet implemented" );
final RootGraph<T> graph = (RootGraph<T>) entityGraph;
final Class<T> entityClass = graph.getGraphedType().getJavaType();
return loadAccessWithOptions( entityClass, options )
.withLoadGraph( graph )
.load( primaryKey );
}
private void checkTransactionNeededForLock(LockModeType lockModeType) {

View File

@ -0,0 +1,128 @@
package org.hibernate.orm.test.jpa.findoptions;
import jakarta.persistence.CacheStoreMode;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityGraph;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.LockModeType;
import jakarta.persistence.Timeout;
import org.hibernate.EnabledFetchProfile;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.ReadOnlyMode;
import org.hibernate.Session;
import org.hibernate.annotations.FetchProfile;
import org.hibernate.annotations.FetchProfileOverride;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.Jpa;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.hibernate.Hibernate.isInitialized;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@Jpa(annotatedClasses = FindOptionsTest.MyEntity.class)
public class FindOptionsTest {
@Test
void test(EntityManagerFactoryScope scope) {
MyEntity hello = new MyEntity("Hello");
scope.getEntityManagerFactory()
.runInTransaction(em -> em.persist(hello));
scope.getEntityManagerFactory()
.runInTransaction(em -> {
MyEntity entity = em.find(MyEntity.class, hello.id,
LockModeType.PESSIMISTIC_WRITE, Timeout.seconds(10),
CacheStoreMode.BYPASS, ReadOnlyMode.READ_WRITE);
assertFalse(em.unwrap(Session.class).isReadOnly(entity));
assertEquals(LockModeType.PESSIMISTIC_WRITE, em.getLockMode(entity));
assertEquals(hello.name, entity.name);
assertFalse(isInitialized(entity.list));
});
scope.getEntityManagerFactory()
.runInTransaction(em -> {
MyEntity entity = em.find(MyEntity.class, hello.id,
LockModeType.PESSIMISTIC_READ, ReadOnlyMode.READ_ONLY,
new EnabledFetchProfile("hello world"));
assertTrue(em.unwrap(Session.class).isReadOnly(entity));
assertEquals(LockModeType.PESSIMISTIC_READ, em.getLockMode(entity));
assertEquals(hello.name, entity.name);
assertTrue(isInitialized(entity.list));
});
scope.getEntityManagerFactory()
.runInTransaction(em -> {
try {
em.find(MyEntity.class, hello.id, LockMode.OPTIMISTIC);
}
catch (HibernateException he) {
return;
}
fail();
});
scope.getEntityManagerFactory()
.runInTransaction(em -> {
try {
em.find(MyEntity.class, hello.id, LockMode.OPTIMISTIC_FORCE_INCREMENT);
}
catch (HibernateException he) {
return;
}
fail();
});
scope.getEntityManagerFactory()
.runInTransaction(em -> {
try {
em.find(MyEntity.class, hello.id, LockMode.PESSIMISTIC_FORCE_INCREMENT);
}
catch (HibernateException he) {
return;
}
fail();
});
scope.getEntityManagerFactory()
.runInTransaction(em -> {
EntityGraph<MyEntity> graph = em.createEntityGraph(MyEntity.class);
MyEntity entity = em.find(graph, hello.id,
LockModeType.PESSIMISTIC_WRITE, ReadOnlyMode.READ_ONLY);
assertTrue(em.unwrap(Session.class).isReadOnly(entity));
assertEquals(LockModeType.PESSIMISTIC_WRITE, em.getLockMode(entity));
assertEquals(hello.name, entity.name);
assertFalse(isInitialized(entity.list));
});
scope.getEntityManagerFactory()
.runInTransaction(em -> {
EntityGraph<MyEntity> graph = em.createEntityGraph(MyEntity.class);
graph.addAttributeNode("list");
MyEntity entity = em.find(graph, hello.id,
LockModeType.PESSIMISTIC_READ, ReadOnlyMode.READ_WRITE);
assertFalse(em.unwrap(Session.class).isReadOnly(entity));
assertEquals(LockModeType.PESSIMISTIC_READ, em.getLockMode(entity));
assertEquals(hello.name, entity.name);
assertTrue(isInitialized(entity.list));
});
}
@Entity
@FetchProfile(name = "hello world")
public static class MyEntity {
@Id @GeneratedValue long id;
String name;
@ElementCollection(fetch = FetchType.LAZY)
@FetchProfileOverride(profile = "hello world",
fetch = FetchType.EAGER)
List<String> list;
public MyEntity(String name) {
this.name = name;
}
MyEntity() {
}
}
}