HHH-14325 - Add Query hint for specifying "query spaces" for native queries

This commit is contained in:
Steve Ebersole 2020-11-12 15:19:55 -06:00
parent 2896372dd5
commit d5067eccf3
9 changed files with 640 additions and 109 deletions

View File

@ -15,10 +15,12 @@ import java.util.Collection;
* processed by auto-flush based on the table to which those entities are mapped and which are * processed by auto-flush based on the table to which those entities are mapped and which are
* determined to have pending state changes. * determined to have pending state changes.
* *
* In a similar manner, these query spaces also affect how query result caching can recognize invalidated results. * In a similar manner, these query spaces also affect how query result caching can recognize
* invalidated results.
* *
* @author Steve Ebersole * @author Steve Ebersole
*/ */
@SuppressWarnings( { "unused", "UnusedReturnValue", "RedundantSuppression" } )
public interface SynchronizeableQuery<T> { public interface SynchronizeableQuery<T> {
/** /**
* Obtain the list of query spaces the query is synchronized on. * Obtain the list of query spaces the query is synchronized on.
@ -36,6 +38,32 @@ public interface SynchronizeableQuery<T> {
*/ */
SynchronizeableQuery<T> addSynchronizedQuerySpace(String querySpace); SynchronizeableQuery<T> addSynchronizedQuerySpace(String querySpace);
/**
* Adds one-or-more synchronized spaces
*/
default SynchronizeableQuery<T> addSynchronizedQuerySpace(String... querySpaces) {
if ( querySpaces != null ) {
for ( int i = 0; i < querySpaces.length; i++ ) {
addSynchronizedQuerySpace( querySpaces[i] );
}
}
return this;
}
/**
* Adds a table expression as a query space.
*/
default SynchronizeableQuery<T> addSynchronizedTable(String tableExpression) {
return addSynchronizedQuerySpace( tableExpression );
}
/**
* Adds one-or-more synchronized table expressions
*/
default SynchronizeableQuery<T> addSynchronizedTable(String... tableExpressions) {
return addSynchronizedQuerySpace( tableExpressions );
}
/** /**
* Adds an entity name for (a) auto-flush checking and (b) query result cache invalidation checking. Same as * Adds an entity name for (a) auto-flush checking and (b) query result cache invalidation checking. Same as
* {@link #addSynchronizedQuerySpace} for all tables associated with the given entity. * {@link #addSynchronizedQuerySpace} for all tables associated with the given entity.
@ -48,6 +76,18 @@ public interface SynchronizeableQuery<T> {
*/ */
SynchronizeableQuery<T> addSynchronizedEntityName(String entityName) throws MappingException; SynchronizeableQuery<T> addSynchronizedEntityName(String entityName) throws MappingException;
/**
* Adds one-or-more entities (by name) whose tables should be added as synchronized spaces
*/
default SynchronizeableQuery<T> addSynchronizedEntityName(String... entityNames) throws MappingException {
if ( entityNames != null ) {
for ( int i = 0; i < entityNames.length; i++ ) {
addSynchronizedEntityName( entityNames[i] );
}
}
return this;
}
/** /**
* Adds an entity for (a) auto-flush checking and (b) query result cache invalidation checking. Same as * Adds an entity for (a) auto-flush checking and (b) query result cache invalidation checking. Same as
* {@link #addSynchronizedQuerySpace} for all tables associated with the given entity. * {@link #addSynchronizedQuerySpace} for all tables associated with the given entity.
@ -58,5 +98,18 @@ public interface SynchronizeableQuery<T> {
* *
* @throws MappingException Indicates the given class could not be resolved as an entity * @throws MappingException Indicates the given class could not be resolved as an entity
*/ */
@SuppressWarnings( "rawtypes" )
SynchronizeableQuery<T> addSynchronizedEntityClass(Class entityClass) throws MappingException; SynchronizeableQuery<T> addSynchronizedEntityClass(Class entityClass) throws MappingException;
/**
* Adds one-or-more entities (by class) whose tables should be added as synchronized spaces
*/
default SynchronizeableQuery<T> addSynchronizedEntityClass(Class<?>... entityClasses) throws MappingException {
if ( entityClasses != null ) {
for ( int i = 0; i < entityClasses.length; i++ ) {
addSynchronizedEntityClass( entityClasses[i] );
}
}
return this;
}
} }

View File

@ -89,4 +89,11 @@ public @interface NamedNativeQuery {
* Whether the results should be read-only. Default is {@code false}. * Whether the results should be read-only. Default is {@code false}.
*/ */
boolean readOnly() default false; boolean readOnly() default false;
/**
* The query spaces to apply for the query.
*
* @see org.hibernate.SynchronizeableQuery
*/
String[] querySpaces() default {};
} }

View File

@ -137,4 +137,17 @@ public class QueryHints {
*/ */
public static final String PASS_DISTINCT_THROUGH = "hibernate.query.passDistinctThrough"; public static final String PASS_DISTINCT_THROUGH = "hibernate.query.passDistinctThrough";
/**
* Hint for specifying query spaces to be applied to a native (SQL) query.
*
* Passed value can be any of:<ul>
* <li>List of the spaces</li>
* <li>array of the spaces</li>
* <li>String "whitespace"-separated list of the spaces</li>
* </ul>
*
* @see org.hibernate.SynchronizeableQuery
*/
public static final String NATIVE_SPACES = "org.hibernate.query.native.spaces";
} }

View File

@ -370,60 +370,68 @@ public final class AnnotationBinder {
context.getMetadataCollector().addIdentifierGenerator( buildIdGenerator( def, context ) ); context.getMetadataCollector().addIdentifierGenerator( buildIdGenerator( def, context ) );
} }
private static void bindQueries(XAnnotatedElement annotatedElement, MetadataBuildingContext context) { private static void bindNamedJpaQueries(XAnnotatedElement annotatedElement, MetadataBuildingContext context) {
{ QueryBinder.bindSqlResultSetMapping(
SqlResultSetMapping ann = annotatedElement.getAnnotation( SqlResultSetMapping.class ); annotatedElement.getAnnotation( SqlResultSetMapping.class ),
QueryBinder.bindSqlResultSetMapping( ann, context, false ); context,
} false
{ );
SqlResultSetMappings ann = annotatedElement.getAnnotation( SqlResultSetMappings.class );
final SqlResultSetMappings ann = annotatedElement.getAnnotation( SqlResultSetMappings.class );
if ( ann != null ) { if ( ann != null ) {
for ( SqlResultSetMapping current : ann.value() ) { for ( SqlResultSetMapping current : ann.value() ) {
QueryBinder.bindSqlResultSetMapping( current, context, false ); QueryBinder.bindSqlResultSetMapping( current, context, false );
} }
} }
}
{ QueryBinder.bindQuery(
NamedQuery ann = annotatedElement.getAnnotation( NamedQuery.class ); annotatedElement.getAnnotation( NamedQuery.class ),
QueryBinder.bindQuery( ann, context, false ); context,
} false
{
org.hibernate.annotations.NamedQuery ann = annotatedElement.getAnnotation(
org.hibernate.annotations.NamedQuery.class
); );
QueryBinder.bindQuery( ann, context );
} QueryBinder.bindQueries(
{ annotatedElement.getAnnotation( NamedQueries.class ),
NamedQueries ann = annotatedElement.getAnnotation( NamedQueries.class ); context,
QueryBinder.bindQueries( ann, context, false ); false
}
{
org.hibernate.annotations.NamedQueries ann = annotatedElement.getAnnotation(
org.hibernate.annotations.NamedQueries.class
); );
QueryBinder.bindQueries( ann, context );
} QueryBinder.bindNativeQuery(
{ annotatedElement.getAnnotation( NamedNativeQuery.class ),
NamedNativeQuery ann = annotatedElement.getAnnotation( NamedNativeQuery.class ); context,
QueryBinder.bindNativeQuery( ann, context, false ); false
}
{
org.hibernate.annotations.NamedNativeQuery ann = annotatedElement.getAnnotation(
org.hibernate.annotations.NamedNativeQuery.class
); );
QueryBinder.bindNativeQuery( ann, context );
} QueryBinder.bindNativeQueries(
{ annotatedElement.getAnnotation( NamedNativeQueries.class ),
NamedNativeQueries ann = annotatedElement.getAnnotation( NamedNativeQueries.class ); context,
QueryBinder.bindNativeQueries( ann, context, false ); false
}
{
org.hibernate.annotations.NamedNativeQueries ann = annotatedElement.getAnnotation(
org.hibernate.annotations.NamedNativeQueries.class
); );
QueryBinder.bindNativeQueries( ann, context );
} }
private static void bindQueries(XAnnotatedElement annotatedElement, MetadataBuildingContext context) {
bindNamedJpaQueries( annotatedElement, context );
QueryBinder.bindQuery(
annotatedElement.getAnnotation( org.hibernate.annotations.NamedQuery.class ),
context
);
QueryBinder.bindQueries(
annotatedElement.getAnnotation( org.hibernate.annotations.NamedQueries.class ),
context
);
QueryBinder.bindNativeQuery(
annotatedElement.getAnnotation( org.hibernate.annotations.NamedNativeQuery.class ),
context
);
QueryBinder.bindNativeQueries(
annotatedElement.getAnnotation( org.hibernate.annotations.NamedNativeQueries.class ),
context
);
// NamedStoredProcedureQuery handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // NamedStoredProcedureQuery handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bindNamedStoredProcedureQuery( bindNamedStoredProcedureQuery(
annotatedElement.getAnnotation( NamedStoredProcedureQuery.class ), annotatedElement.getAnnotation( NamedStoredProcedureQuery.class ),

View File

@ -48,11 +48,15 @@ public abstract class QueryBinder {
NamedQuery queryAnn, NamedQuery queryAnn,
MetadataBuildingContext context, MetadataBuildingContext context,
boolean isDefault) { boolean isDefault) {
if ( queryAnn == null ) return; if ( queryAnn == null ) {
return;
}
if ( BinderHelper.isEmptyAnnotationValue( queryAnn.name() ) ) { if ( BinderHelper.isEmptyAnnotationValue( queryAnn.name() ) ) {
throw new AnnotationException( "A named query must have a name when used in class or package level" ); throw new AnnotationException( "A named query must have a name when used in class or package level" );
} }
//EJBQL Query
// JPA-QL Query
QueryHintDefinition hints = new QueryHintDefinition( queryAnn.hints() ); QueryHintDefinition hints = new QueryHintDefinition( queryAnn.hints() );
String queryName = queryAnn.query(); String queryName = queryAnn.query();
NamedQueryDefinition queryDefinition = new NamedQueryDefinitionBuilder( queryAnn.name() ) NamedQueryDefinition queryDefinition = new NamedQueryDefinitionBuilder( queryAnn.name() )
@ -112,14 +116,17 @@ public abstract class QueryBinder {
if ( !BinderHelper.isEmptyAnnotationValue( resultSetMapping ) ) { if ( !BinderHelper.isEmptyAnnotationValue( resultSetMapping ) ) {
//sql result set usage //sql result set usage
builder.setResultSetRef( resultSetMapping ) builder.setResultSetRef( resultSetMapping ).createNamedQueryDefinition();
.createNamedQueryDefinition();
} }
else if ( !void.class.equals( queryAnn.resultClass() ) ) { else if ( !void.class.equals( queryAnn.resultClass() ) ) {
//class mapping usage //class mapping usage
//FIXME should be done in a second pass due to entity name? //FIXME should be done in a second pass due to entity name?
final NativeSQLQueryRootReturn entityQueryReturn = final NativeSQLQueryRootReturn entityQueryReturn = new NativeSQLQueryRootReturn(
new NativeSQLQueryRootReturn( "alias1", queryAnn.resultClass().getName(), new HashMap(), LockMode.READ ); "alias1",
queryAnn.resultClass().getName(),
new HashMap(),
LockMode.READ
);
builder.setQueryReturns( new NativeSQLQueryReturn[] {entityQueryReturn} ); builder.setQueryReturns( new NativeSQLQueryReturn[] {entityQueryReturn} );
} }
else { else {
@ -151,19 +158,16 @@ public abstract class QueryBinder {
throw new AnnotationException( "A named query must have a name when used in class or package level" ); throw new AnnotationException( "A named query must have a name when used in class or package level" );
} }
NamedSQLQueryDefinition query; final String resultSetMapping = queryAnn.resultSetMapping();
String resultSetMapping = queryAnn.resultSetMapping();
if ( !BinderHelper.isEmptyAnnotationValue( resultSetMapping ) ) { final NamedSQLQueryDefinitionBuilder builder = new NamedSQLQueryDefinitionBuilder()
//sql result set usage .setName( queryAnn.name() )
query = new NamedSQLQueryDefinitionBuilder().setName( queryAnn.name() )
.setQuery( queryAnn.query() ) .setQuery( queryAnn.query() )
.setResultSetRef( resultSetMapping )
.setQuerySpaces( null )
.setCacheable( queryAnn.cacheable() ) .setCacheable( queryAnn.cacheable() )
.setCacheRegion( .setCacheRegion(
BinderHelper.isEmptyAnnotationValue( queryAnn.cacheRegion() ) ? BinderHelper.isEmptyAnnotationValue( queryAnn.cacheRegion() )
null : ? null
queryAnn.cacheRegion() : queryAnn.cacheRegion()
) )
.setTimeout( queryAnn.timeout() < 0 ? null : queryAnn.timeout() ) .setTimeout( queryAnn.timeout() < 0 ? null : queryAnn.timeout() )
.setFetchSize( queryAnn.fetchSize() < 0 ? null : queryAnn.fetchSize() ) .setFetchSize( queryAnn.fetchSize() < 0 ? null : queryAnn.fetchSize() )
@ -172,38 +176,32 @@ public abstract class QueryBinder {
.setReadOnly( queryAnn.readOnly() ) .setReadOnly( queryAnn.readOnly() )
.setComment( BinderHelper.isEmptyAnnotationValue( queryAnn.comment() ) ? null : queryAnn.comment() ) .setComment( BinderHelper.isEmptyAnnotationValue( queryAnn.comment() ) ? null : queryAnn.comment() )
.setParameterTypes( null ) .setParameterTypes( null )
.setCallable( queryAnn.callable() ) .setCallable( queryAnn.callable() );
.createNamedQueryDefinition();
if ( !BinderHelper.isEmptyAnnotationValue( resultSetMapping ) ) {
//sql result set usage
builder.setResultSetRef( resultSetMapping );
} }
else if ( ! void.class.equals( queryAnn.resultClass() ) ) { else if ( ! void.class.equals( queryAnn.resultClass() ) ) {
//class mapping usage //class mapping usage
//FIXME should be done in a second pass due to entity name? //FIXME should be done in a second pass due to entity name?
final NativeSQLQueryRootReturn entityQueryReturn = final NativeSQLQueryRootReturn entityQueryReturn = new NativeSQLQueryRootReturn(
new NativeSQLQueryRootReturn( "alias1", queryAnn.resultClass().getName(), new HashMap(), LockMode.READ ); "alias1",
query = new NamedSQLQueryDefinitionBuilder().setName( queryAnn.name() ) queryAnn.resultClass().getName(),
.setQuery( queryAnn.query() ) new HashMap(),
.setQueryReturns( new NativeSQLQueryReturn[] {entityQueryReturn} ) LockMode.READ
.setQuerySpaces( null ) );
.setCacheable( queryAnn.cacheable() ) builder.setQueryReturns( new NativeSQLQueryReturn[] {entityQueryReturn} );
.setCacheRegion(
BinderHelper.isEmptyAnnotationValue( queryAnn.cacheRegion() ) ?
null :
queryAnn.cacheRegion()
)
.setTimeout( queryAnn.timeout() < 0 ? null : queryAnn.timeout() )
.setFetchSize( queryAnn.fetchSize() < 0 ? null : queryAnn.fetchSize() )
.setFlushMode( getFlushMode( queryAnn.flushMode() ) )
.setCacheMode( getCacheMode( queryAnn.cacheMode() ) )
.setReadOnly( queryAnn.readOnly() )
.setComment( BinderHelper.isEmptyAnnotationValue( queryAnn.comment() ) ? null : queryAnn.comment() )
.setParameterTypes( null )
.setCallable( queryAnn.callable() )
.createNamedQueryDefinition();
} }
else { else {
throw new NotYetImplementedException( "Pure native scalar queries are not yet supported" ); LOG.debugf( "Raw scalar native-query (no explicit result mappings) found : %s", queryAnn.name() );
} }
final NamedSQLQueryDefinition query = builder.createNamedQueryDefinition();
context.getMetadataCollector().addNamedNativeQuery( query ); context.getMetadataCollector().addNamedNativeQuery( query );
if ( LOG.isDebugEnabled() ) { if ( LOG.isDebugEnabled() ) {
LOG.debugf( "Binding named native query: %s => %s", query.getName(), queryAnn.query() ); LOG.debugf( "Binding named native query: %s => %s", query.getName(), queryAnn.query() );
} }

View File

@ -19,6 +19,7 @@ import static org.hibernate.annotations.QueryHints.FLUSH_MODE;
import static org.hibernate.annotations.QueryHints.FOLLOW_ON_LOCKING; import static org.hibernate.annotations.QueryHints.FOLLOW_ON_LOCKING;
import static org.hibernate.annotations.QueryHints.LOADGRAPH; import static org.hibernate.annotations.QueryHints.LOADGRAPH;
import static org.hibernate.annotations.QueryHints.NATIVE_LOCKMODE; import static org.hibernate.annotations.QueryHints.NATIVE_LOCKMODE;
import static org.hibernate.annotations.QueryHints.NATIVE_SPACES;
import static org.hibernate.annotations.QueryHints.PASS_DISTINCT_THROUGH; import static org.hibernate.annotations.QueryHints.PASS_DISTINCT_THROUGH;
import static org.hibernate.annotations.QueryHints.READ_ONLY; import static org.hibernate.annotations.QueryHints.READ_ONLY;
import static org.hibernate.annotations.QueryHints.TIMEOUT_HIBERNATE; import static org.hibernate.annotations.QueryHints.TIMEOUT_HIBERNATE;
@ -26,8 +27,6 @@ import static org.hibernate.annotations.QueryHints.TIMEOUT_JPA;
/** /**
* Defines the supported JPA query hints * Defines the supported JPA query hints
*
* @author Steve Ebersole
*/ */
public class QueryHints { public class QueryHints {
/** /**
@ -105,10 +104,13 @@ public class QueryHints {
public static final String HINT_PASS_DISTINCT_THROUGH = PASS_DISTINCT_THROUGH; public static final String HINT_PASS_DISTINCT_THROUGH = PASS_DISTINCT_THROUGH;
public static final String HINT_NATIVE_SPACES = NATIVE_SPACES;
private static final Set<String> HINTS = buildHintsSet(); private static final Set<String> HINTS = buildHintsSet();
private static Set<String> buildHintsSet() { private static Set<String> buildHintsSet() {
HashSet<String> hints = new HashSet<String>(); HashSet<String> hints = new HashSet<>();
hints.add( HINT_TIMEOUT ); hints.add( HINT_TIMEOUT );
hints.add( SPEC_HINT_TIMEOUT ); hints.add( SPEC_HINT_TIMEOUT );
hints.add( HINT_COMMENT ); hints.add( HINT_COMMENT );
@ -121,6 +123,7 @@ public class QueryHints {
hints.add( HINT_NATIVE_LOCKMODE ); hints.add( HINT_NATIVE_LOCKMODE );
hints.add( HINT_FETCHGRAPH ); hints.add( HINT_FETCHGRAPH );
hints.add( HINT_LOADGRAPH ); hints.add( HINT_LOADGRAPH );
hints.add( HINT_NATIVE_SPACES );
return java.util.Collections.unmodifiableSet( hints ); return java.util.Collections.unmodifiableSet( hints );
} }

View File

@ -27,9 +27,7 @@ import java.util.Spliterator;
import java.util.Spliterators; import java.util.Spliterators;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.StreamSupport; import java.util.stream.StreamSupport;
import javax.persistence.CacheRetrieveMode; import javax.persistence.CacheRetrieveMode;
@ -101,6 +99,7 @@ import static org.hibernate.jpa.QueryHints.HINT_FETCH_SIZE;
import static org.hibernate.jpa.QueryHints.HINT_FLUSH_MODE; import static org.hibernate.jpa.QueryHints.HINT_FLUSH_MODE;
import static org.hibernate.jpa.QueryHints.HINT_FOLLOW_ON_LOCKING; import static org.hibernate.jpa.QueryHints.HINT_FOLLOW_ON_LOCKING;
import static org.hibernate.jpa.QueryHints.HINT_LOADGRAPH; import static org.hibernate.jpa.QueryHints.HINT_LOADGRAPH;
import static org.hibernate.jpa.QueryHints.HINT_NATIVE_SPACES;
import static org.hibernate.jpa.QueryHints.HINT_READONLY; import static org.hibernate.jpa.QueryHints.HINT_READONLY;
import static org.hibernate.jpa.QueryHints.HINT_TIMEOUT; import static org.hibernate.jpa.QueryHints.HINT_TIMEOUT;
import static org.hibernate.jpa.QueryHints.SPEC_HINT_TIMEOUT; import static org.hibernate.jpa.QueryHints.SPEC_HINT_TIMEOUT;
@ -1109,6 +1108,9 @@ public abstract class AbstractProducedQuery<R> implements QueryImplementor<R> {
final CacheStoreMode storeMode = value != null ? CacheStoreMode.valueOf( value.toString() ) : null; final CacheStoreMode storeMode = value != null ? CacheStoreMode.valueOf( value.toString() ) : null;
applied = applyJpaCacheStoreMode( storeMode ); applied = applyJpaCacheStoreMode( storeMode );
} }
else if ( HINT_NATIVE_SPACES.equals( hintName ) ) {
applied = applyQuerySpaces( value );
}
else if ( QueryHints.HINT_NATIVE_LOCKMODE.equals( hintName ) ) { else if ( QueryHints.HINT_NATIVE_LOCKMODE.equals( hintName ) ) {
applied = applyNativeQueryLockMode( value ); applied = applyNativeQueryLockMode( value );
} }
@ -1160,6 +1162,12 @@ public abstract class AbstractProducedQuery<R> implements QueryImplementor<R> {
return this; return this;
} }
protected boolean applyQuerySpaces(Object value) {
throw new IllegalStateException(
"Illegal attempt to apply native-query spaces to a non-native query"
);
}
protected void handleUnrecognizedHint(String hintName, Object value) { protected void handleUnrecognizedHint(String hintName, Object value) {
MSG_LOGGER.debugf( "Skipping unsupported query hint [%s]", hintName ); MSG_LOGGER.debugf( "Skipping unsupported query hint [%s]", hintName );
} }

View File

@ -19,6 +19,7 @@ import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.StringTokenizer;
import javax.persistence.FlushModeType; import javax.persistence.FlushModeType;
import javax.persistence.LockModeType; import javax.persistence.LockModeType;
import javax.persistence.Parameter; import javax.persistence.Parameter;
@ -32,6 +33,7 @@ import org.hibernate.LockOptions;
import org.hibernate.MappingException; import org.hibernate.MappingException;
import org.hibernate.QueryException; import org.hibernate.QueryException;
import org.hibernate.ScrollMode; import org.hibernate.ScrollMode;
import org.hibernate.SynchronizeableQuery;
import org.hibernate.engine.ResultSetMappingDefinition; import org.hibernate.engine.ResultSetMappingDefinition;
import org.hibernate.engine.query.spi.EntityGraphQueryHint; import org.hibernate.engine.query.spi.EntityGraphQueryHint;
import org.hibernate.engine.query.spi.sql.NativeSQLQueryConstructorReturn; import org.hibernate.engine.query.spi.sql.NativeSQLQueryConstructorReturn;
@ -42,6 +44,7 @@ import org.hibernate.engine.spi.NamedSQLQueryDefinition;
import org.hibernate.engine.spi.QueryParameters; import org.hibernate.engine.spi.QueryParameters;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.StringHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
import org.hibernate.query.NativeQuery; import org.hibernate.query.NativeQuery;
import org.hibernate.query.ParameterMetadata; import org.hibernate.query.ParameterMetadata;
import org.hibernate.query.QueryParameter; import org.hibernate.query.QueryParameter;
@ -246,7 +249,7 @@ public class NativeQueryImpl<T> extends AbstractProducedQuery<T> implements Nati
super.beforeQuery(); super.beforeQuery();
if ( getSynchronizedQuerySpaces() != null && !getSynchronizedQuerySpaces().isEmpty() ) { if ( CollectionHelper.isNotEmpty( getSynchronizedQuerySpaces() ) ) {
// The application defined query spaces on the Hibernate native SQLQuery which means the query will already // The application defined query spaces on the Hibernate native SQLQuery which means the query will already
// perform a partial flush according to the defined query spaces, no need to do a full flush. // perform a partial flush according to the defined query spaces, no need to do a full flush.
return; return;
@ -438,12 +441,18 @@ public class NativeQueryImpl<T> extends AbstractProducedQuery<T> implements Nati
return this; return this;
} }
@Override
public SynchronizeableQuery<T> addSynchronizedQuerySpace(String... querySpaces) {
addQuerySpaces( querySpaces );
return this;
}
protected void addQuerySpaces(String... spaces) { protected void addQuerySpaces(String... spaces) {
if ( spaces != null ) { if ( spaces != null ) {
if ( querySpaces == null ) { if ( querySpaces == null ) {
querySpaces = new ArrayList<>(); querySpaces = new ArrayList<>();
} }
querySpaces.addAll( Arrays.asList( (String[]) spaces ) ); querySpaces.addAll( Arrays.asList( spaces ) );
} }
} }
@ -468,6 +477,36 @@ public class NativeQueryImpl<T> extends AbstractProducedQuery<T> implements Nati
return this; return this;
} }
@Override
protected boolean applyQuerySpaces(Object value) {
if ( value == null ) {
return false;
}
if ( value instanceof String[] ) {
addSynchronizedQuerySpace( (String[]) value );
return true;
}
if ( value instanceof Collection ) {
// if ( querySpaces == null ) {
// querySpaces = new ArrayList<>();
// }
querySpaces.addAll( (Collection<String>) value );
return true;
}
if ( value instanceof String ) {
final StringTokenizer spaces = new StringTokenizer( (String) value, "," );
while ( spaces.hasMoreTokens() ) {
addQuerySpaces( spaces.nextToken() );
}
return true;
}
return false;
}
@Override @Override
protected boolean isNativeQuery() { protected boolean isNativeQuery() {
return true; return true;

View File

@ -0,0 +1,402 @@
/*
* 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.sql;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.EntityResult;
import javax.persistence.Id;
import javax.persistence.Query;
import javax.persistence.QueryHint;
import javax.persistence.SqlResultSetMapping;
import javax.persistence.Table;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.NamedNativeQuery;
import org.hibernate.cache.spi.CacheImplementor;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.jpa.QueryHints;
import org.hibernate.query.spi.NativeQueryImplementor;
import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* @author Steve Ebersole
*/
public class SynchronizedSpaceTests extends BaseNonConfigCoreFunctionalTestCase {
@Test
public void testNonSyncedCachedScenario() {
// CachedEntity updated by native-query without adding query spaces
// - the outcome should be all cached data being invalidated
checkUseCase(
"cached_entity",
query -> {},
// the 2 CachedEntity entries should not be there
false
);
// and of course, let's make sure the update happened :)
inTransaction(
session -> {
session.createQuery( "from CachedEntity", CachedEntity.class ).list().forEach(
cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) )
);
}
);
}
private void checkUseCase(
String table,
Consumer<Query> queryConsumer,
boolean shouldExistAfter) {
checkUseCase(
(session) -> {
final Query nativeQuery = session.createNativeQuery( "update " + table + " set name = 'updated'" );
queryConsumer.accept( nativeQuery );
return nativeQuery;
},
Query::executeUpdate,
shouldExistAfter
);
}
private void checkUseCase(
Function<SessionImplementor,Query> queryProducer,
Consumer<Query> executor,
boolean shouldExistAfter) {
// first, load both `CachedEntity` instances into the L2 cache
loadAll();
final CacheImplementor cacheSystem = sessionFactory().getCache();
// make sure they are there
assertThat( cacheSystem.containsEntity( CachedEntity.class, 1 ), is( true ) );
assertThat( cacheSystem.containsEntity( CachedEntity.class, 2 ), is( true ) );
// create a query to update the specified table - allowing the passed consumer to register a space if needed
inTransaction(
session -> {
// notice the type is the JPA Query interface
final Query nativeQuery = queryProducer.apply( session );
executor.accept( nativeQuery );
}
);
// see if the entries exist based on the expectation
assertThat( cacheSystem.containsEntity( CachedEntity.class, 1 ), is( shouldExistAfter ) );
assertThat( cacheSystem.containsEntity( CachedEntity.class, 2 ), is( shouldExistAfter ) );
}
@Test
public void testSyncedCachedScenario() {
final String tableName = "cached_entity";
checkUseCase(
tableName,
query -> ( (NativeQueryImplementor<?>) query ).addSynchronizedQuerySpace( tableName ),
// the 2 CachedEntity entries should not be there
false
);
// and of course, let's make sure the update happened :)
inTransaction(
session -> {
session.createQuery( "from CachedEntity", CachedEntity.class ).list().forEach(
cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) )
);
}
);
}
@Test
public void testNonSyncedNonCachedScenario() {
// NonCachedEntity updated by native-query without adding query spaces
// - the outcome should be all cached data being invalidated
checkUseCase(
"non_cached_entity",
query -> {},
// the 2 CachedEntity entries should not be there
false
);
// and of course, let's make sure the update happened :)
inTransaction(
session -> {
session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach(
cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) )
);
}
);
}
@Test
public void testSyncedNonCachedScenario() {
// NonCachedEntity updated by native-query with query spaces
// - the caches for CachedEntity are not invalidated - they are not affected by the specified query-space
final String tableName = "non_cached_entity";
checkUseCase(
tableName,
query -> ( (NativeQueryImplementor<?>) query ).addSynchronizedQuerySpace( tableName ),
// the 2 CachedEntity entries should still be there
true
);
// and of course, let's make sure the update happened :)
inTransaction(
session -> {
session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach(
cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) )
);
}
);
}
@Test
public void testSyncedNonCachedScenarioUsingHint() {
// same as `#testSyncedNonCachedScenario`, but here using the hint
final String tableName = "non_cached_entity";
checkUseCase(
tableName,
query -> query.setHint( QueryHints.HINT_NATIVE_SPACES, tableName ),
// the 2 CachedEntity entries should still be there
true
);
// and of course, let's make sure the update happened :)
inTransaction(
session -> {
session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach(
cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) )
);
}
);
}
@Test
public void testSyncedNonCachedScenarioUsingHintWithCollection() {
// same as `#testSyncedNonCachedScenario`, but here using the hint
final String tableName = "non_cached_entity";
final Set<String> spaces = new HashSet<>();
spaces.add( tableName );
checkUseCase(
tableName,
query -> query.setHint( QueryHints.HINT_NATIVE_SPACES, spaces ),
// the 2 CachedEntity entries should still be there
true
);
// and of course, let's make sure the update happened :)
inTransaction(
session -> {
session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach(
cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) )
);
}
);
}
@Test
public void testSyncedNonCachedScenarioUsingHintWithArray() {
// same as `#testSyncedNonCachedScenario`, but here using the hint
final String tableName = "non_cached_entity";
final String[] spaces = { tableName };
checkUseCase(
tableName,
query -> query.setHint( QueryHints.HINT_NATIVE_SPACES, spaces ),
// the 2 CachedEntity entries should still be there
true
);
// and of course, let's make sure the update happened :)
inTransaction(
session -> {
session.createQuery( "from NonCachedEntity", NonCachedEntity.class ).list().forEach(
cachedEntity -> assertThat( cachedEntity.name, is( "updated" ) )
);
}
);
}
@Test
public void testSyncedNonCachedScenarioUsingAnnotationWithReturnClass() {
checkUseCase(
(session) -> session.createNamedQuery( "NonCachedEntity_return_class" ),
Query::getResultList,
true
);
}
@Test
public void testSyncedNonCachedScenarioUsingAnnotationWithResultSetMapping() {
checkUseCase(
(session) -> session.createNamedQuery( "NonCachedEntity_resultset_mapping" ),
Query::getResultList,
true
);
}
@Test
public void testSyncedNonCachedScenarioUsingAnnotationWithSpaces() {
checkUseCase(
(session) -> session.createNamedQuery( "NonCachedEntity_spaces" ),
Query::getResultList,
true
);
}
@Test
public void testSyncedNonCachedScenarioUsingJpaAnnotationWithNoResultMapping() {
checkUseCase(
(session) -> session.createNamedQuery( "NonCachedEntity_raw_jpa" ),
Query::getResultList,
true
);
}
@Test
public void testSyncedNonCachedScenarioUsingJpaAnnotationWithHint() {
checkUseCase(
(session) -> session.createNamedQuery( "NonCachedEntity_hint_jpa" ),
Query::getResultList,
true
);
}
private void loadAll() {
inTransaction(
session -> {
session.createQuery( "from CachedEntity" ).list();
// this one is not strictly needed since this entity is not cached.
// but it helps my OCD feel better to have it ;)
session.createQuery( "from NonCachedEntity" ).list();
}
);
}
public void prepareTest() {
inTransaction(
session -> {
session.persist( new CachedEntity( 1, "first cached" ) );
session.persist( new CachedEntity( 2, "second cached" ) );
session.persist( new NonCachedEntity( 1, "first non-cached" ) );
session.persist( new NonCachedEntity( 2, "second non-cached" ) );
}
);
cleanupCache();
}
public void cleanupTest() {
cleanupCache();
inTransaction(
session -> {
session.createQuery( "delete CachedEntity" ).executeUpdate();
session.createQuery( "delete NonCachedEntity" ).executeUpdate();
}
);
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { CachedEntity.class, NonCachedEntity.class };
}
@Override
protected boolean overrideCacheStrategy() {
return false;
}
@Entity( name = "CachedEntity" )
@Table( name = "cached_entity" )
@Cacheable( true )
@Cache( usage = CacheConcurrencyStrategy.READ_WRITE )
public static class CachedEntity {
@Id
private Integer id;
private String name;
public CachedEntity() {
}
public CachedEntity(Integer id, String name) {
this.id = id;
this.name = name;
}
}
@Entity( name = "NonCachedEntity" )
@Table( name = "non_cached_entity" )
@Cacheable( false )
@NamedNativeQuery(
name = "NonCachedEntity_return_class",
query = "select * from non_cached_entity",
resultClass = NonCachedEntity.class
)
@NamedNativeQuery(
name = "NonCachedEntity_resultset_mapping",
query = "select * from non_cached_entity",
resultSetMapping = "NonCachedEntity_resultset_mapping"
)
@SqlResultSetMapping(
name = "NonCachedEntity_resultset_mapping",
entities = @EntityResult( entityClass = NonCachedEntity.class )
)
@NamedNativeQuery(
name = "NonCachedEntity_spaces",
query = "select * from non_cached_entity",
querySpaces = "non_cached_entity"
)
@javax.persistence.NamedNativeQuery(
name = "NonCachedEntity_raw_jpa",
query = "select * from non_cached_entity"
)
@javax.persistence.NamedNativeQuery(
name = "NonCachedEntity_hint_jpa",
query = "select * from non_cached_entity",
hints = {
@QueryHint( name = QueryHints.HINT_NATIVE_SPACES, value = "non_cached_entity" )
}
)
public static class NonCachedEntity {
@Id
private Integer id;
private String name;
public NonCachedEntity() {
}
public NonCachedEntity(Integer id, String name) {
this.id = id;
this.name = name;
}
}
}