* Verified named "result mapping" support - e.g. JPA's `@SqlResultMapping`; gets stored correctly, but can't test it getting applied correctly until after native-query support is implemented

* Verified / implemented query-result cache support
This commit is contained in:
Steve Ebersole 2020-04-29 13:12:24 -05:00
parent ac4af03ea3
commit 1f1f5f118b
22 changed files with 566 additions and 72 deletions

View File

@ -98,40 +98,51 @@ public class QueryKey implements Serializable {
private int generateHashCode() {
int result = 13;
result = 37 * result + sqlQueryString.hashCode();
result = 37 * result + ( firstRow==null ? 0 : firstRow.hashCode() );
result = 37 * result + ( maxRows==null ? 0 : maxRows.hashCode() );
result = 37 * result + parameterBindingsMemento.hashCode();
result = 37 * result + ( enabledFilterNames == null ? 0 : enabledFilterNames.hashCode() );
result = 37 * result + ( firstRow==null ? 0 : firstRow );
result = 37 * result + ( maxRows==null ? 0 : maxRows );
result = 37 * result + ( tenantIdentifier==null ? 0 : tenantIdentifier.hashCode() );
// the collections are too complicated to incorporate into the hashcode. but they really
// aren't needed in the hashcode calculation - they are handled in `#equals` and the calculation
// without them is a good hashing code.
//
// todo (6.0) : maybe even just base it on `sqlQueryString`?
return result;
}
@Override
@SuppressWarnings("RedundantIfStatement")
@SuppressWarnings({"RedundantIfStatement", "EqualsWhichDoesntCheckParameterClass"})
public boolean equals(Object other) {
if ( !( other instanceof QueryKey ) ) {
// it should never be another type, so skip the instanceof check and just do the cast
final QueryKey that;
try {
that = (QueryKey) other;
}
catch (ClassCastException cce) {
// treat this as the exception case
return false;
}
final QueryKey that = (QueryKey) other;
if ( !sqlQueryString.equals( that.sqlQueryString ) ) {
if ( ! Objects.equals( sqlQueryString, that.sqlQueryString ) ) {
return false;
}
if ( !Objects.equals( tenantIdentifier, that.tenantIdentifier ) ) {
if ( ! Objects.equals( tenantIdentifier, that.tenantIdentifier ) ) {
return false;
}
if ( !Objects.equals( firstRow, that.firstRow )
|| !Objects.equals( maxRows, that.maxRows ) ) {
if ( ! Objects.equals( firstRow, that.firstRow )
|| ! Objects.equals( maxRows, that.maxRows ) ) {
return false;
}
if ( !Objects.equals( parameterBindingsMemento, that.parameterBindingsMemento ) ) {
// Set's `#equals` impl does a deep check, so `Objects#equals` is a good check
if ( ! Objects.equals( parameterBindingsMemento, that.parameterBindingsMemento ) ) {
return false;
}
if ( !Objects.equals( enabledFilterNames, that.enabledFilterNames ) ) {
if ( ! Objects.equals( enabledFilterNames, that.enabledFilterNames ) ) {
return false;
}

View File

@ -24,10 +24,9 @@ import static org.jboss.logging.Logger.Level.WARN;
@MessageLogger( projectCode = "HHH" )
@ValidIdRange( min = 90001001, max = 90002000 )
public interface SecondLevelCacheLogger extends BasicLogger {
SecondLevelCacheLogger INSTANCE = Logger.getMessageLogger(
SecondLevelCacheLogger.class,
"org.hibernate.orm.cache"
);
String LOGGER_NAME = "org.hibernate.orm.cache";
SecondLevelCacheLogger INSTANCE = Logger.getMessageLogger( SecondLevelCacheLogger.class, LOGGER_NAME );
boolean DEBUG_ENABLED = INSTANCE.isDebugEnabled();
boolean TRACE_ENABLED = INSTANCE.isTraceEnabled();

View File

@ -91,7 +91,7 @@ public abstract class AbstractScrollableResults<R> implements ScrollableResultsI
return;
}
getJdbcValues().finishUp();
getJdbcValues().finishUp( persistenceContext );
getPersistenceContext().getJdbcCoordinator().afterStatementExecution();
this.closed = true;

View File

@ -12,8 +12,10 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import org.hibernate.Incubating;
import org.hibernate.NotYetImplementedFor6Exception;
import org.hibernate.QueryException;
import org.hibernate.QueryParameterException;
import org.hibernate.cache.spi.QueryKey;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.query.QueryParameter;
@ -174,4 +176,24 @@ public class QueryParameterBindingsImpl implements QueryParameterBindings {
}
);
}
@Override
public QueryKey.ParameterBindingsMemento generateQueryKeyMemento() {
// todo (6.0) : need to decide how to handle
if ( parameterMetadata.getParameterCount() == 0 && CollectionHelper.isEmpty( parameterBindingMap ) ) {
return new QueryKey.ParameterBindingsMemento() {
@Override
public int hashCode() {
return QueryParameterBindingsImpl.class.hashCode();
}
@Override
public boolean equals(Object obj) {
return obj instanceof QueryKey.ParameterBindingsMemento;
}
};
}
throw new NotYetImplementedFor6Exception( getClass() );
}
}

View File

@ -10,7 +10,7 @@ package org.hibernate.query.named;
* @author Steve Ebersole
*/
public interface RowReaderMemento {
Class[] getResultClasses();
Class<?>[] getResultClasses();
String[] getResultMappingNames();
}

View File

@ -23,17 +23,18 @@ 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;
import org.hibernate.sql.exec.SqlExecLogger;
import org.hibernate.sql.exec.spi.ExecutionContext;
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
import org.hibernate.sql.exec.spi.JdbcSelect;
import org.hibernate.sql.exec.spi.JdbcSelectExecutor;
import org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess;
import org.hibernate.sql.results.internal.Helper;
import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl;
import org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess;
import org.hibernate.sql.results.jdbc.internal.JdbcValuesCacheHit;
import org.hibernate.sql.results.jdbc.internal.JdbcValuesResultSetImpl;
import org.hibernate.sql.results.jdbc.internal.JdbcValuesSourceProcessingStateStandardImpl;
import org.hibernate.sql.results.jdbc.internal.ResultSetAccess;
import org.hibernate.sql.results.internal.RowProcessingStateStandardImpl;
import org.hibernate.sql.results.jdbc.spi.JdbcValues;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesMapping;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions;
@ -43,8 +44,6 @@ import org.hibernate.sql.results.spi.RowReader;
import org.hibernate.sql.results.spi.RowTransformer;
import org.hibernate.sql.results.spi.ScrollableResultsConsumer;
import org.jboss.logging.Logger;
/**
* @author Steve Ebersole
*/
@ -61,8 +60,6 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
*/
public static final JdbcSelectExecutorStandardImpl INSTANCE = new JdbcSelectExecutorStandardImpl();
private static final Logger log = Logger.getLogger( JdbcSelectExecutorStandardImpl.class );
@Override
public <R> List<R> list(
JdbcSelect jdbcSelect,
@ -254,7 +251,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
final QueryKey queryResultsCacheKey;
if ( queryCacheEnabled && cacheMode.isGetEnabled() ) {
log.debugf( "Reading Query result cache data per CacheMode#isGetEnabled [%s]", cacheMode.name() );
SqlExecLogger.INSTANCE.debugf( "Reading Query result cache data per CacheMode#isGetEnabled [%s]", cacheMode.name() );
final QueryResultsCache queryCache = executionContext.getSession().getFactory()
.getCache()
@ -290,7 +287,7 @@ public class JdbcSelectExecutorStandardImpl implements JdbcSelectExecutor {
// invalidation strategies - QueryCacheInvalidationStrategy
}
else {
log.debugf( "Skipping reading Query result cache data: cache-enabled = %s, cache-mode = %s",
SqlExecLogger.INSTANCE.debugf( "Skipping reading Query result cache data: cache-enabled = %s, cache-mode = %s",
queryCacheEnabled,
cacheMode.name()
);

View File

@ -6,11 +6,13 @@
*/
package org.hibernate.sql.results.caching;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
/**
* @author Steve Ebersole
*/
public interface QueryCachePutManager {
void registerJdbcRow(Object[] values);
void finishUp();
void finishUp(SharedSessionContractImplementor session);
}

View File

@ -6,6 +6,7 @@
*/
package org.hibernate.sql.results.caching.internal;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.sql.results.caching.QueryCachePutManager;
/**
@ -28,7 +29,7 @@ public class QueryCachePutManagerDisabledImpl implements QueryCachePutManager {
}
@Override
public void finishUp() {
public void finishUp(SharedSessionContractImplementor session) {
}
}

View File

@ -7,10 +7,12 @@
package org.hibernate.sql.results.caching.internal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.hibernate.cache.spi.QueryKey;
import org.hibernate.cache.spi.QueryResultsCache;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.sql.results.caching.QueryCachePutManager;
/**
@ -35,16 +37,21 @@ public class QueryCachePutManagerEnabledImpl implements QueryCachePutManager {
if ( dataToCache == null ) {
dataToCache = new ArrayList<>();
}
dataToCache.add( values );
// todo (6.0) : verify whether we really need to copy these..
// `RowProcessingStateStandardImpl` (see `#finishRowProcessing`) already creates new array
// instances for each row
// dataToCache.add( values );
dataToCache.add( Arrays.copyOf( values, values.length ) );
}
@Override
public void finishUp() {
public void finishUp(SharedSessionContractImplementor session) {
// todo (
queryCache.put(
queryKey,
dataToCache,
// todo (6.0) : needs access to Session to pass along to cache call
null
session
);
}
}

View File

@ -49,23 +49,15 @@ public class Helper {
}
private static Consumer<Initializer> getInitializerConsumer(List<Initializer> initializers) {
if ( ResultsLogger.INSTANCE.isDebugEnabled() ) {
return initializer -> {
ResultsLogger.INSTANCE.debug( "Adding initializer : " + initializer );
addIfNotPresent( initializers, initializer );
};
}
else {
return initializer ->
addIfNotPresent( initializers, initializer );
}
}
return initializer -> {
ResultsLogger.INSTANCE.debugf( "Initializer registration : %s", initializer );
if ( initializers.contains( initializer ) ) {
ResultsLogger.INSTANCE.debug( "Skipping initializer registration - already registered" );
}
private static void addIfNotPresent(List<Initializer> initializers, Initializer initializer) {
if ( initializers.contains( initializer ) ) {
return;
}
initializers.add( initializer );
ResultsLogger.INSTANCE.debugf( "Adding initializer : %s", initializer );
initializers.add( initializer );
};
}
public static void finalizeCollectionLoading(

View File

@ -36,6 +36,7 @@ public class RowProcessingStateStandardImpl implements RowProcessingState {
private final Initializer[] initializers;
private final RowReader<?> rowReader;
private final JdbcValues jdbcValues;
private Object[] currentRowJdbcValues;
@ -46,6 +47,7 @@ public class RowProcessingStateStandardImpl implements RowProcessingState {
JdbcValues jdbcValues) {
this.resultSetProcessingState = resultSetProcessingState;
this.queryOptions = queryOptions;
this.rowReader = rowReader;
this.jdbcValues = jdbcValues;
final List<Initializer> initializers = rowReader.getInitializers();
@ -63,6 +65,11 @@ public class RowProcessingStateStandardImpl implements RowProcessingState {
return resultSetProcessingState;
}
@Override
public RowReader<?> getRowReader() {
return rowReader;
}
public boolean next() throws SQLException {
if ( jdbcValues.next( this ) ) {
currentRowJdbcValues = jdbcValues.getCurrentRowValuesArray();

View File

@ -11,9 +11,9 @@ import java.util.List;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.query.named.RowReaderMemento;
import org.hibernate.sql.exec.spi.Callback;
import org.hibernate.sql.results.graph.collection.CollectionInitializer;
import org.hibernate.sql.results.graph.DomainResultAssembler;
import org.hibernate.sql.results.graph.Initializer;
import org.hibernate.sql.results.graph.collection.CollectionInitializer;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingState;
import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
@ -25,6 +25,7 @@ import org.jboss.logging.Logger;
/**
* @author Steve Ebersole
*/
@SuppressWarnings("rawtypes")
public class StandardRowReader<T> implements RowReader<T> {
private static final Logger LOG = Logger.getLogger( StandardRowReader.class );
@ -35,7 +36,6 @@ public class StandardRowReader<T> implements RowReader<T> {
private final int assemblerCount;
private final Callback callback;
private final Object[] resultRow;
@SuppressWarnings("WeakerAccess")
public StandardRowReader(
@ -49,15 +49,13 @@ public class StandardRowReader<T> implements RowReader<T> {
this.assemblerCount = resultAssemblers.size();
this.callback = callback;
this.resultRow = new Object[assemblerCount];
}
@Override
@SuppressWarnings("unchecked")
public Class<T> getResultJavaType() {
if ( resultAssemblers.size() == 1 ) {
return resultAssemblers.get( 0 ).getAssembledJavaTypeDescriptor().getJavaType();
return (Class<T>) resultAssemblers.get( 0 ).getAssembledJavaTypeDescriptor().getJavaType();
}
return (Class<T>) Object[].class;
@ -74,6 +72,8 @@ public class StandardRowReader<T> implements RowReader<T> {
coordinateInitializers( rowProcessingState, options );
final Object[] resultRow = new Object[ assemblerCount ];
for ( int i = 0; i < assemblerCount; i++ ) {
resultRow[i] = resultAssemblers.get( i ).assemble( rowProcessingState, options );
}
@ -91,6 +91,7 @@ public class StandardRowReader<T> implements RowReader<T> {
}
}
@SuppressWarnings("ForLoopReplaceableByForEach")
private void coordinateInitializers(
RowProcessingState rowProcessingState,
JdbcValuesSourceProcessingOptions options) {
@ -143,6 +144,7 @@ public class StandardRowReader<T> implements RowReader<T> {
}
@Override
@SuppressWarnings("ForLoopReplaceableByForEach")
public void finishUp(JdbcValuesSourceProcessingState processingState) {
for ( int i = 0; i < initializers.size(); i++ ) {
initializers.get( i ).endLoading( processingState.getExecutionContext() );
@ -156,7 +158,7 @@ public class StandardRowReader<T> implements RowReader<T> {
public RowReaderMemento toMemento(SessionFactoryImplementor factory) {
return new RowReaderMemento() {
@Override
public Class[] getResultClasses() {
public Class<?>[] getResultClasses() {
return new Class[0];
}

View File

@ -8,6 +8,7 @@ package org.hibernate.sql.results.jdbc.internal;
import java.sql.SQLException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.sql.results.caching.QueryCachePutManager;
import org.hibernate.sql.results.jdbc.spi.JdbcValues;
import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
@ -27,17 +28,19 @@ public abstract class AbstractJdbcValues implements JdbcValues {
@Override
public final boolean next(RowProcessingState rowProcessingState) throws SQLException {
if ( getCurrentRowValuesArray() != null ) {
final boolean hadRow = processNext( rowProcessingState );
if ( hadRow ) {
queryCachePutManager.registerJdbcRow( getCurrentRowValuesArray() );
}
return processNext( rowProcessingState );
return hadRow;
}
protected abstract boolean processNext(RowProcessingState rowProcessingState);
@Override
public final void finishUp() {
queryCachePutManager.finishUp();
public final void finishUp(SharedSessionContractImplementor session) {
queryCachePutManager.finishUp( session );
release();
}

View File

@ -8,6 +8,8 @@ package org.hibernate.sql.results.jdbc.internal;
import java.util.List;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.sql.results.ResultsLogger;
import org.hibernate.sql.results.caching.internal.QueryCachePutManagerDisabledImpl;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesMapping;
import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
@ -18,14 +20,19 @@ import org.hibernate.sql.results.jdbc.spi.RowProcessingState;
* @author Steve Ebersole
*/
public class JdbcValuesCacheHit extends AbstractJdbcValues {
private static final Object[][] NO_DATA = new Object[0][];
private Object[][] cachedData;
private final int numberOfRows;
private JdbcValuesMapping resolvedMapping;
private int position = -1;
public JdbcValuesCacheHit(Object[][] cachedData, JdbcValuesMapping resolvedMapping) {
// if we have a cache hit we should not be writting back to the cache.
// if we have a cache hit we should not be writing back to the cache.
// its silly because the state would always be the same.
//
// well actually, there are times when we want to write values back to the cache even though we had a hit...
// the case is related to the domain-data cache
super( QueryCachePutManagerDisabledImpl.INSTANCE );
this.cachedData = cachedData;
this.numberOfRows = cachedData.length;
@ -33,25 +40,38 @@ public class JdbcValuesCacheHit extends AbstractJdbcValues {
}
public JdbcValuesCacheHit(List<Object[]> cachedResults, JdbcValuesMapping resolvedMapping) {
this( (Object[][]) cachedResults.toArray(), resolvedMapping );
this( extractData( cachedResults ), resolvedMapping );
}
private static Object[][] extractData(List<Object[]> cachedResults) {
if ( CollectionHelper.isEmpty( cachedResults ) ) {
return NO_DATA;
}
final Object[][] data = new Object[cachedResults.size()][];
for ( int i = 0; i < cachedResults.size(); i++ ) {
final Object[] row = cachedResults.get( i );
data[ i ] = row;
}
return data;
}
@Override
protected boolean processNext(RowProcessingState rowProcessingState) {
// NOTE : explicitly skipping limit handling under the truth that
// because the cached state ought to be the same size since
// the cache key includes limits
if ( isExhausted() ) {
ResultsLogger.INSTANCE.tracef( "JdbcValuesCacheHit#processNext : position = %i; numberOfRows = %i", position, numberOfRows );
// NOTE : explicitly skipping limit handling because the cached state ought
// already be the limited size since the cache key includes limits
if ( position >= numberOfRows - 1 ) {
return false;
}
position++;
return true;
}
private boolean isExhausted() {
return position >= numberOfRows;
}
@Override
public JdbcValuesMapping getValuesMapping() {
return resolvedMapping;
@ -59,7 +79,7 @@ public class JdbcValuesCacheHit extends AbstractJdbcValues {
@Override
public Object[] getCurrentRowValuesArray() {
if ( isExhausted() ) {
if ( position >= numberOfRows ) {
return null;
}
return cachedData[position];

View File

@ -8,6 +8,8 @@ package org.hibernate.sql.results.jdbc.spi;
import java.sql.SQLException;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
/**
* Provides unified access to query results (JDBC values - see
* {@link RowProcessingState#getJdbcValue} whether they come from
@ -47,6 +49,7 @@ public interface JdbcValues {
* ^^ it's supposed to give impls a chance to write to the query cache
* or release ResultSet it. But that could technically be handled by the
* case of `#next` returning false the first time.
* @param session
*/
void finishUp();
void finishUp(SharedSessionContractImplementor session);
}

View File

@ -11,6 +11,7 @@ import org.hibernate.sql.ast.spi.SqlSelection;
import org.hibernate.sql.exec.spi.ExecutionContext;
import org.hibernate.sql.results.graph.Initializer;
import org.hibernate.sql.results.graph.entity.EntityFetch;
import org.hibernate.sql.results.spi.RowReader;
/**
* State pertaining to the processing of a single "row" of a JdbcValuesSource
@ -34,6 +35,11 @@ public interface RowProcessingState extends ExecutionContext {
return getJdbcValue( sqlSelection.getValuesArrayPosition() );
}
/**
* todo (6.0) : do we want this here? Depends how we handle caching assembler / result memento
*/
RowReader<?> getRowReader();
/**
* Retrieve the value corresponding to the given index as part
* of the "current JDBC row".

View File

@ -90,7 +90,7 @@ public class ListResultsConsumer<R> implements ResultsConsumer<List<R>, R> {
finally {
rowReader.finishUp( jdbcValuesSourceProcessingState );
jdbcValuesSourceProcessingState.finishUp();
jdbcValues.finishUp();
jdbcValues.finishUp( session );
}
}
}

View File

@ -0,0 +1,51 @@
/*
* 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.orm.test.query.named.resultmapping;
import javax.persistence.ColumnResult;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.SqlResultSetMapping;
/**
* @author Steve Ebersole
*/
@Entity
@SqlResultSetMapping(
name = "name",
columns = @ColumnResult( name = "name" )
)
public class SimpleEntityWithNamedMappings {
@Id
private Integer id;
private String name;
protected SimpleEntityWithNamedMappings() {
}
public SimpleEntityWithNamedMappings(Integer id, String name) {
this.id = id;
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

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.orm.test.query.named.resultmapping;
import org.hibernate.query.named.NamedQueryRepository;
import org.hibernate.query.named.NamedResultSetMappingMemento;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.FailureExpected;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author Steve Ebersole
*/
@DomainModel( annotatedClasses = SimpleEntityWithNamedMappings.class )
@SessionFactory
public class SimpleNamedMappingTests {
@Test
@FailureExpected( reason = "Memento-ization of row-reader not yet implemented" )
public void testMapping(SessionFactoryScope sessionFactoryScope) {
final QueryEngine queryEngine = sessionFactoryScope.getSessionFactory().getQueryEngine();
final NamedQueryRepository namedQueryRepository = queryEngine.getNamedQueryRepository();
final NamedResultSetMappingMemento mappingMemento = namedQueryRepository.getResultSetMappingMemento( "name" );
assertThat( mappingMemento.toResultSetMapping(), notNullValue() );
}
// todo (6.0) : atm that ^^ is as far as we can test until we implement native-query support to test applying a mapping
}

View File

@ -0,0 +1,72 @@
/*
* 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.orm.test.query.resultcache;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
/**
* @author Steve Ebersole
*/
@Entity
public class AggregateEntity {
@Id
private Integer id;
private String name;
@ManyToOne(cascade = CascadeType.ALL)
private TestEntity value1;
@ManyToOne(cascade = CascadeType.ALL)
private TestEntity value2;
protected AggregateEntity() {
}
public AggregateEntity(
Integer id,
String name,
TestEntity value1,
TestEntity value2) {
this.id = id;
this.name = name;
this.value1 = value1;
this.value2 = value2;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestEntity getValue1() {
return value1;
}
public void setValue1(TestEntity value1) {
this.value1 = value1;
}
public TestEntity getValue2() {
return value2;
}
public void setValue2(TestEntity value2) {
this.value2 = value2;
}
}

View File

@ -0,0 +1,199 @@
/*
* 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.orm.test.query.resultcache;
import java.util.List;
import org.hibernate.CacheMode;
import org.hibernate.cache.spi.SecondLevelCacheLogger;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.sql.exec.SqlExecLogger;
import org.hibernate.sql.results.ResultsLogger;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author Steve Ebersole
*/
@ServiceRegistry(
settings = {
@ServiceRegistry.Setting( name = AvailableSettings.USE_QUERY_CACHE, value = "true" )
}
)
@DomainModel( annotatedClasses = { TestEntity.class, AggregateEntity.class } )
@SessionFactory( generateStatistics = true )
public class QueryResultCacheTests {
private final Logger resultsLogger = Logger.getLogger( ResultsLogger.LOGGER_NAME );
private final Logger execLogger = Logger.getLogger( SqlExecLogger.LOGGER_NAME );
private final Logger cacheLogger = Logger.getLogger( SecondLevelCacheLogger.LOGGER_NAME );
private Level originalResultsLevel;
private Level originalExecLevel;
private Level originalCacheLevel;
@BeforeAll
public void setUpLogger() {
originalResultsLevel = resultsLogger.getLevel();
resultsLogger.setLevel( Level.TRACE );
originalExecLevel = execLogger.getLevel();
execLogger.setLevel( Level.TRACE );
originalCacheLevel = cacheLogger.getLevel();
cacheLogger.setLevel( Level.TRACE );
}
@AfterAll
public void resetLogger() {
resultsLogger.setLevel( originalResultsLevel );
execLogger.setLevel( originalExecLevel );
cacheLogger.setLevel( originalCacheLevel );
}
@Test
public void testScalarCaching(SessionFactoryScope scope) {
final StatisticsImplementor statistics = scope.getSessionFactory().getStatistics();
statistics.clear();
assertThat( statistics.getPrepareStatementCount(), is( 0L ) );
final String hql = "select e.id, e.name from TestEntity e order by e.id";
scope.inTransaction(
session -> {
resultsLogger.debug( "First query ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
final List values = session.createQuery( hql )
.setCacheable( true )
.setCacheMode( CacheMode.NORMAL )
.setCacheRegion( "scalar-region" )
.list();
assertThat( statistics.getPrepareStatementCount(), is( 1L ) );
verifyScalarResults( values );
}
);
scope.inTransaction(
session -> {
resultsLogger.debug( "Second query ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
final List values = session.createQuery( hql )
.setCacheable( true )
.setCacheMode( CacheMode.NORMAL )
.setCacheRegion( "scalar-region" )
.list();
assertThat( statistics.getPrepareStatementCount(), is( 1L ) );
verifyScalarResults( values );
}
);
}
private void verifyScalarResults(List values) {
assertThat( values.size(), is( 2 ) );
final Object[] firstRow = (Object[]) values.get( 0 );
assertThat( firstRow[0], is( 1 ) );
assertThat( firstRow[1], is( "first" ) );
final Object[] secondRow = (Object[]) values.get( 1 );
assertThat( secondRow[0], is( 2 ) );
assertThat( secondRow[1], is( "second" ) );
}
@Test
public void testJoinFetchCaching(SessionFactoryScope scope) {
final StatisticsImplementor statistics = scope.getSessionFactory().getStatistics();
statistics.clear();
assertThat( statistics.getPrepareStatementCount(), is( 0L ) );
final String hql = "select e from AggregateEntity e join fetch e.value1 join fetch e.value2";
scope.inTransaction(
session -> {
resultsLogger.debug( "First query ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
final List<AggregateEntity> values = session.createQuery( hql, AggregateEntity.class )
.setCacheable( true )
.setCacheMode( CacheMode.NORMAL )
.setCacheRegion( "fetch-region" )
.list();
assertThat( statistics.getPrepareStatementCount(), is( 1L ) );
verifyFetchResults( values );
}
);
scope.inTransaction(
session -> {
resultsLogger.debug( "Second query ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
final List<AggregateEntity> values = session.createQuery( hql, AggregateEntity.class )
.setCacheable( true )
.setCacheMode( CacheMode.NORMAL )
.setCacheRegion( "fetch-region" )
.list();
assertThat( statistics.getPrepareStatementCount(), is( 1L ) );
verifyFetchResults( values );
}
);
}
private void verifyFetchResults(List<AggregateEntity> values) {
assertThat( values.size(), is( 1 ) );
final AggregateEntity rootEntity = values.get( 0 );
assertThat( rootEntity.getValue1(), notNullValue() );
assertThat( rootEntity.getValue1().getId(), is( 1 ) );
assertThat( rootEntity.getValue2(), notNullValue() );
assertThat( rootEntity.getValue2().getId(), is( 2 ) );
}
@BeforeEach
public void prepareTestData(SessionFactoryScope scope) {
scope.inTransaction(
session -> session.save(
new AggregateEntity(
1,
"aggregate",
new TestEntity( 1, "first" ),
new TestEntity( 2, "second" )
)
)
);
}
@AfterEach
public void cleanupTestData(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
session.createQuery( "delete AggregateEntity" ).executeUpdate();
session.createQuery( "delete TestEntity" ).executeUpdate();
}
);
}
}

View File

@ -0,0 +1,62 @@
/*
* 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.orm.test.query.resultcache;
import java.time.Instant;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
/**
* @author Steve Ebersole
*/
@Entity
public class TestEntity {
@Id
private Integer id;
private String name;
@Temporal( TemporalType.TIMESTAMP )
private Instant instant;
public TestEntity() {
}
public TestEntity(Integer id, String name) {
this( id, name, Instant.now() );
}
public TestEntity(Integer id, String name, Instant instant) {
this.id = id;
this.name = name;
this.instant = instant;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Instant getInstant() {
return instant;
}
public void setInstant(Instant instant) {
this.instant = instant;
}
}