implement readonly loader feature

This commit is contained in:
Nathan Xu 2020-03-22 23:15:48 -04:00 committed by Steve Ebersole
parent ced4f5e602
commit 23719ff481
15 changed files with 292 additions and 109 deletions

View File

@ -21,6 +21,7 @@
import org.hibernate.metamodel.mapping.EntityMappingType;
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.spi.QueryOptionsAdapter;
import org.hibernate.query.spi.QueryParameterBindings;
import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
@ -55,7 +56,7 @@ public SingleIdEntityLoaderDynamicBatch(
}
@Override
public T load(Object pkValue, LockOptions lockOptions, SharedSessionContractImplementor session) {
public T load(Object pkValue, LockOptions lockOptions, Boolean readOnly, SharedSessionContractImplementor session) {
final Object[] batchIds = session.getPersistenceContextInternal()
.getBatchFetchQueue()
.getBatchLoadableEntityIds( getLoadable(), pkValue, maxBatchSize );
@ -64,7 +65,7 @@ public T load(Object pkValue, LockOptions lockOptions, SharedSessionContractImpl
if ( numberOfIds <= 1 ) {
initializeSingleIdLoaderIfNeeded( session );
final T result = singleIdLoader.load( pkValue, lockOptions, session );
final T result = singleIdLoader.load( pkValue, lockOptions, readOnly, session );
if ( result == null ) {
// There was no entity with the specified ID. Make sure the EntityKey does not remain
// in the batch to avoid including it in future batches that get executed.
@ -147,7 +148,12 @@ public SharedSessionContractImplementor getSession() {
@Override
public QueryOptions getQueryOptions() {
return QueryOptions.NONE;
return new QueryOptionsAdapter() {
@Override
public Boolean isReadOnly() {
return readOnly;
}
};
}
@Override
@ -181,9 +187,10 @@ public T load(
Object pkValue,
Object entityInstance,
LockOptions lockOptions,
Boolean readOnly,
SharedSessionContractImplementor session) {
initializeSingleIdLoaderIfNeeded( session );
return singleIdLoader.load( pkValue, entityInstance, lockOptions, session );
return singleIdLoader.load( pkValue, entityInstance, lockOptions, readOnly, session );
}
private void initializeSingleIdLoaderIfNeeded(SharedSessionContractImplementor session) {

View File

@ -57,7 +57,7 @@ public EntityMappingType getLoadable() {
}
@Override
public T load(Object pkValue, LockOptions lockOptions, SharedSessionContractImplementor session) {
public T load(Object pkValue, LockOptions lockOptions, Boolean readOnly, SharedSessionContractImplementor session) {
//noinspection unchecked
final QueryImplementor<T> query = namedQueryMemento.toQuery(
session,
@ -74,11 +74,12 @@ public T load(
Object pkValue,
Object entityInstance,
LockOptions lockOptions,
Boolean readOnly,
SharedSessionContractImplementor session) {
if ( entityInstance != null ) {
throw new UnsupportedOperationException( );
}
return load( pkValue, lockOptions, session );
return load( pkValue, lockOptions, readOnly, session );
}
@Override

View File

@ -52,14 +52,14 @@ public void prepare() {
}
@Override
public T load(Object key, LockOptions lockOptions, SharedSessionContractImplementor session) {
public T load(Object key, LockOptions lockOptions, Boolean readOnly, SharedSessionContractImplementor session) {
final SingleIdLoadPlan<T> loadPlan = resolveLoadPlan(
lockOptions,
session.getLoadQueryInfluencers(),
session.getFactory()
);
return loadPlan.load( key, lockOptions, null, session );
return loadPlan.load( key, lockOptions, readOnly, session );
}
@Override
@ -67,6 +67,7 @@ public T load(
Object key,
Object entityInstance,
LockOptions lockOptions,
Boolean readOnly,
SharedSessionContractImplementor session) {
final SingleIdLoadPlan<T> loadPlan = resolveLoadPlan(
lockOptions,
@ -74,7 +75,7 @@ public T load(
session.getFactory()
);
return loadPlan.load( key, lockOptions, entityInstance, session );
return loadPlan.load( key, lockOptions, entityInstance, readOnly, session );
}
@Internal

View File

@ -18,6 +18,7 @@
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.ModelPart;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.spi.QueryOptionsAdapter;
import org.hibernate.query.spi.QueryParameterBindings;
import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
@ -73,14 +74,23 @@ public SelectStatement getSqlAst() {
T load(
Object restrictedValue,
LockOptions lockOptions,
Boolean readOnly,
SharedSessionContractImplementor session) {
return load( restrictedValue, lockOptions, null, session );
return load( restrictedValue, lockOptions, null, readOnly, session );
}
T load(
Object restrictedValue,
LockOptions lockOptions,
SharedSessionContractImplementor session) {
return load( restrictedValue, lockOptions, null, null, session );
}
T load(
Object restrictedValue,
LockOptions lockOptions,
Object entityInstance,
Boolean readOnly,
SharedSessionContractImplementor session) {
final SessionFactoryImplementor sessionFactory = session.getFactory();
final JdbcServices jdbcServices = sessionFactory.getJdbcServices();
@ -145,7 +155,12 @@ public Object getEntityId() {
@Override
public QueryOptions getQueryOptions() {
return QueryOptions.NONE;
return new QueryOptionsAdapter() {
@Override
public Boolean isReadOnly() {
return readOnly;
}
};
}
@Override

View File

@ -22,6 +22,7 @@
import org.hibernate.metamodel.mapping.JdbcMapping;
import org.hibernate.metamodel.mapping.SingularAttributeMapping;
import org.hibernate.query.spi.QueryOptions;
import org.hibernate.query.spi.QueryOptionsAdapter;
import org.hibernate.query.spi.QueryParameterBindings;
import org.hibernate.sql.ast.Clause;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
@ -57,6 +58,7 @@ public EntityMappingType getLoadable() {
public T load(
Object ukValue,
LockOptions lockOptions,
Boolean readOnly,
SharedSessionContractImplementor session) {
final SessionFactoryImplementor sessionFactory = session.getFactory();
@ -117,7 +119,12 @@ public SharedSessionContractImplementor getSession() {
@Override
public QueryOptions getQueryOptions() {
return QueryOptions.NONE;
return new QueryOptionsAdapter() {
@Override
public Boolean isReadOnly() {
return readOnly;
}
};
}
@Override

View File

@ -23,4 +23,7 @@ public interface CollectionLoader extends Loader {
* Load a collection by its key (not necessarily the same as its owner's PK).
*/
PersistentCollection load(Object key, SharedSessionContractImplementor session);
//TODO support 'readOnly' collection loading
}

View File

@ -22,5 +22,9 @@ public interface SingleEntityLoader<T> extends Loader {
/**
* Load an entity by a primary or unique key value.
*/
T load(Object key, LockOptions lockOptions, SharedSessionContractImplementor session);
T load(Object key, LockOptions lockOptions, Boolean readOnly, SharedSessionContractImplementor session);
default T load(Object key, LockOptions lockOptions, SharedSessionContractImplementor session) {
return load( key, lockOptions, session );
}
}

View File

@ -19,14 +19,18 @@ public interface SingleIdEntityLoader<T> extends SingleEntityLoader<T> {
* Load by primary key value
*/
@Override
T load(Object pkValue, LockOptions lockOptions, SharedSessionContractImplementor session);
T load(Object pkValue, LockOptions lockOptions, Boolean readOnly, SharedSessionContractImplementor session);
/**
* Load by primary key value, populating the passed entity instance. Used to initialize an uninitialized
* bytecode-proxy or {@link org.hibernate.event.spi.LoadEvent} handling.
* The passed instance is the enhanced proxy or the entity to be loaded.
*/
T load(Object pkValue, Object entityInstance, LockOptions lockOptions, SharedSessionContractImplementor session);
T load(Object pkValue, Object entityInstance, LockOptions lockOptions, Boolean readOnly, SharedSessionContractImplementor session);
default T load(Object pkValue, Object entityInstance, LockOptions lockOptions, SharedSessionContractImplementor session) {
return load( pkValue, entityInstance, lockOptions, null, session );
}
/**
* Load database snapshot by primary key value

View File

@ -19,7 +19,7 @@ public interface SingleUniqueKeyEntityLoader<T> extends SingleEntityLoader {
* Load by unique key value
*/
@Override
T load(Object ukValue, LockOptions lockOptions, SharedSessionContractImplementor session);
T load(Object ukValue, LockOptions lockOptions, Boolean readOnly, SharedSessionContractImplementor session);
/**
* Resolve the matching id

View File

@ -2617,7 +2617,15 @@ public Object loadByUniqueKey(
String propertyName,
Object uniqueKey,
SharedSessionContractImplementor session) throws HibernateException {
return getUniqueKeyLoader( propertyName ).load( uniqueKey, LockOptions.READ, session );
return loadByUniqueKey( propertyName, uniqueKey, null, session );
}
public Object loadByUniqueKey(
String propertyName,
Object uniqueKey,
Boolean readOnly,
SharedSessionContractImplementor session) throws HibernateException {
return getUniqueKeyLoader( propertyName ).load( uniqueKey, LockOptions.READ, readOnly, session );
}
private Map<SingularAttributeMapping, SingleUniqueKeyEntityLoader<?>> uniqueKeyLoadersNew;
@ -4459,25 +4467,25 @@ public Object load(Object id, Object optionalObject, LockMode lockMode, SharedSe
*/
public Object load(Object id, Object optionalObject, LockOptions lockOptions, SharedSessionContractImplementor session)
throws HibernateException {
return doLoad( id, optionalObject, lockOptions, session, null );
return doLoad( id, optionalObject, lockOptions, null, session );
}
public Object load(Object id, Object optionalObject, LockOptions lockOptions, SharedSessionContractImplementor session, Boolean readOnly)
throws HibernateException {
return doLoad( id, optionalObject, lockOptions, session, readOnly );
return doLoad( id, optionalObject, lockOptions, readOnly, session );
}
private Object doLoad(Object id, Object optionalObject, LockOptions lockOptions, SharedSessionContractImplementor session, Boolean readOnly)
private Object doLoad(Object id, Object optionalObject, LockOptions lockOptions, Boolean readOnly, SharedSessionContractImplementor session)
throws HibernateException {
if ( LOG.isTraceEnabled() ) {
LOG.tracev( "Fetching entity: {0}", MessageHelper.infoString( this, id, getFactory() ) );
}
if ( optionalObject == null ) {
return singleIdEntityLoader.load( id, lockOptions, session );
return singleIdEntityLoader.load( id, lockOptions, readOnly, session );
}
else {
return singleIdEntityLoader.load( id, optionalObject, lockOptions, session );
return singleIdEntityLoader.load( id, optionalObject, lockOptions, readOnly, session );
}
}

View File

@ -16,10 +16,10 @@
* @since 5.2
*/
@Incubating
public class ScrollableResultsIterator<T> implements CloseableIterator {
private final ScrollableResultsImplementor scrollableResults;
public class ScrollableResultsIterator<T> implements CloseableIterator<T> {
private final ScrollableResultsImplementor<T> scrollableResults;
public ScrollableResultsIterator(ScrollableResultsImplementor scrollableResults) {
public ScrollableResultsIterator(ScrollableResultsImplementor<T> scrollableResults) {
this.scrollableResults = scrollableResults;
}
@ -34,7 +34,6 @@ public boolean hasNext() {
}
@Override
@SuppressWarnings("unchecked")
public T next() {
return (T) scrollableResults.get();
}

View File

@ -7,7 +7,6 @@
package org.hibernate.query.spi;
import java.sql.Statement;
import java.util.Collections;
import java.util.List;
import javax.persistence.CacheRetrieveMode;
import javax.persistence.CacheStoreMode;
@ -184,85 +183,6 @@ default boolean hasLimit() {
/**
* Singleton access
*/
QueryOptions NONE = new QueryOptions() {
@Override
public Limit getLimit() {
return Limit.NONE;
}
@Override
public Integer getFetchSize() {
return null;
}
@Override
public String getComment() {
return null;
}
@Override
public LockOptions getLockOptions() {
return LockOptions.NONE;
}
@Override
public List<String> getDatabaseHints() {
return Collections.emptyList();
}
@Override
public Integer getTimeout() {
return null;
}
@Override
public FlushMode getFlushMode() {
return null;
}
@Override
public Boolean isReadOnly() {
return null;
}
@Override
public CacheRetrieveMode getCacheRetrieveMode() {
return CacheRetrieveMode.BYPASS;
}
@Override
public CacheStoreMode getCacheStoreMode() {
return CacheStoreMode.BYPASS;
}
@Override
public CacheMode getCacheMode() {
return CacheMode.IGNORE;
}
@Override
public Boolean isResultCachingEnabled() {
return null;
}
@Override
public String getResultCacheRegionName() {
return null;
}
@Override
public AppliedGraph getAppliedGraph() {
return null;
}
@Override
public TupleTransformer getTupleTransformer() {
return null;
}
@Override
public ResultListTransformer getResultListTransformer() {
return null;
}
QueryOptions NONE = new QueryOptionsAdapter() {
};
}

View File

@ -0,0 +1,103 @@
/*
* 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.query.spi;
import java.util.Collections;
import java.util.List;
import javax.persistence.CacheRetrieveMode;
import javax.persistence.CacheStoreMode;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
import org.hibernate.LockOptions;
import org.hibernate.graph.spi.AppliedGraph;
import org.hibernate.query.Limit;
import org.hibernate.query.ResultListTransformer;
import org.hibernate.query.TupleTransformer;
public abstract class QueryOptionsAdapter implements QueryOptions {
@Override
public Limit getLimit() {
return Limit.NONE;
}
@Override
public Integer getFetchSize() {
return null;
}
@Override
public String getComment() {
return null;
}
@Override
public LockOptions getLockOptions() {
return LockOptions.NONE;
}
@Override
public List<String> getDatabaseHints() {
return Collections.emptyList();
}
@Override
public Integer getTimeout() {
return null;
}
@Override
public FlushMode getFlushMode() {
return null;
}
@Override
public Boolean isReadOnly() {
return null;
}
@Override
public CacheRetrieveMode getCacheRetrieveMode() {
return CacheRetrieveMode.BYPASS;
}
@Override
public CacheStoreMode getCacheStoreMode() {
return CacheStoreMode.BYPASS;
}
@Override
public CacheMode getCacheMode() {
return CacheMode.IGNORE;
}
@Override
public Boolean isResultCachingEnabled() {
return null;
}
@Override
public String getResultCacheRegionName() {
return null;
}
@Override
public AppliedGraph getAppliedGraph() {
return null;
}
@Override
public TupleTransformer getTupleTransformer() {
return null;
}
@Override
public ResultListTransformer getResultListTransformer() {
return null;
}
}

View File

@ -19,6 +19,7 @@
import org.hibernate.ScrollMode;
import org.hibernate.cache.spi.QueryKey;
import org.hibernate.cache.spi.QueryResultsCache;
import org.hibernate.engine.spi.PersistenceContext;
import org.hibernate.loader.ast.spi.AfterLoadAction;
import org.hibernate.query.internal.ScrollableResultsIterator;
import org.hibernate.query.spi.ScrollableResultsImplementor;
@ -78,7 +79,7 @@ public <R> List<R> list(
.getJdbcCoordinator()
.getStatementPreparer()
.prepareStatement( sql ),
ListResultsConsumer.instance(uniqueFilter)
ListResultsConsumer.instance( uniqueFilter )
);
}
@ -130,6 +131,37 @@ private <T, R> T executeQuery(
RowTransformer<R> rowTransformer,
Function<String, PreparedStatement> statementCreator,
ResultsConsumer<T, R> resultsConsumer) {
final PersistenceContext persistenceContext = executionContext.getSession().getPersistenceContext();
boolean defaultReadOnlyOrig = persistenceContext.isDefaultReadOnly();
Boolean readOnly = executionContext.getQueryOptions().isReadOnly();
if ( readOnly != null ) {
// The read-only/modifiable mode for the query was explicitly set.
// Temporarily set the default read-only/modifiable setting to the query's setting.
persistenceContext.setDefaultReadOnly( readOnly );
}
try {
return doExecuteQuery(
jdbcSelect,
jdbcParameterBindings,
executionContext,
rowTransformer,
statementCreator,
resultsConsumer
);
}
finally {
if ( readOnly != null ) {
persistenceContext.setDefaultReadOnly( defaultReadOnlyOrig );
}
}
}
private <T, R> T doExecuteQuery(
JdbcSelect jdbcSelect,
JdbcParameterBindings jdbcParameterBindings,
ExecutionContext executionContext,
RowTransformer<R> rowTransformer,
Function<String, PreparedStatement> statementCreator,
ResultsConsumer<T, R> resultsConsumer) {
final JdbcValues jdbcValues = resolveJdbcValuesSource(
jdbcSelect,
@ -203,7 +235,6 @@ public boolean shouldReturnProxies() {
afterLoadAction.afterLoad( executionContext.getSession(), null, null );
}
return result;
}

View File

@ -0,0 +1,80 @@
package org.hibernate.orm.test.loading;
import java.util.Collections;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.jpa.QueryHints;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@DomainModel(
annotatedClasses = {
ReadonlyHintTest.SimpleEntity.class
}
)
@SessionFactory
@TestForIssue( jiraKey = "HHH-11958" )
public class ReadonlyHintTest {
private static final String ORIGINAL_NAME = "original";
private static final String CHANGED_NAME = "changed";
@BeforeEach
void setUp(SessionFactoryScope scope) {
scope.inTransaction( session -> {
SimpleEntity entity = new SimpleEntity();
entity.id = 1L;
entity.name = ORIGINAL_NAME;
session.persist( entity );
} );
}
@Test
void testWithReadOnlyHint(SessionFactoryScope scope) {
scope.inTransaction( session -> {
SimpleEntity fetchedEntity = session.find( SimpleEntity.class, 1L, Collections.singletonMap( QueryHints.HINT_READONLY, true ) );
fetchedEntity.name = CHANGED_NAME;
} );
scope.inTransaction( session -> {
SimpleEntity fetchedEntity = session.find( SimpleEntity.class, 1L );
assertThat(fetchedEntity.name, is( ORIGINAL_NAME ) );
} );
}
@Test
void testWithoutReadOnlyHint(SessionFactoryScope scope) {
scope.inTransaction( session -> {
SimpleEntity fetchedEntity = session.find( SimpleEntity.class, 1L );
fetchedEntity.name = CHANGED_NAME;
} );
scope.inTransaction( session -> {
SimpleEntity fetchedEntity = session.find( SimpleEntity.class, 1L );
assertThat(fetchedEntity.name, is( CHANGED_NAME ) );
} );
}
@AfterEach
void tearDown(SessionFactoryScope scope) {
scope.inTransaction( session -> session.createQuery( "delete from SimpleEntity" ).executeUpdate() );
}
@Entity(name = "SimpleEntity")
public static class SimpleEntity {
@Id
private Long id;
private String name;
}
}