From 49ae7bdff073c7078fea72e5650bddaa96be988a Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Thu, 12 Nov 2020 15:19:55 -0600 Subject: [PATCH] HHH-14325 - Add Query hint for specifying "query spaces" for native queries --- .../org/hibernate/SynchronizeableQuery.java | 55 ++- .../annotations/NamedNativeQuery.java | 7 + .../org/hibernate/annotations/QueryHints.java | 13 + .../org/hibernate/cfg/AnnotationBinder.java | 110 ++--- .../cfg/annotations/QueryBinder.java | 98 +++-- .../java/org/hibernate/jpa/QueryHints.java | 15 +- .../query/internal/AbstractProducedQuery.java | 12 +- .../query/internal/NativeQueryImpl.java | 43 +- .../query/sql/SynchronizedSpaceTests.java | 402 ++++++++++++++++++ 9 files changed, 640 insertions(+), 115 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/SynchronizedSpaceTests.java diff --git a/hibernate-core/src/main/java/org/hibernate/SynchronizeableQuery.java b/hibernate-core/src/main/java/org/hibernate/SynchronizeableQuery.java index da1206e8d7ca..fb1d4f7ec1a7 100644 --- a/hibernate-core/src/main/java/org/hibernate/SynchronizeableQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/SynchronizeableQuery.java @@ -15,10 +15,12 @@ * processed by auto-flush based on the table to which those entities are mapped and which are * 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 */ +@SuppressWarnings( { "unused", "UnusedReturnValue", "RedundantSuppression" } ) public interface SynchronizeableQuery { /** * Obtain the list of query spaces the query is synchronized on. @@ -36,6 +38,32 @@ public interface SynchronizeableQuery { */ SynchronizeableQuery addSynchronizedQuerySpace(String querySpace); + /** + * Adds one-or-more synchronized spaces + */ + default SynchronizeableQuery 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 addSynchronizedTable(String tableExpression) { + return addSynchronizedQuerySpace( tableExpression ); + } + + /** + * Adds one-or-more synchronized table expressions + */ + default SynchronizeableQuery addSynchronizedTable(String... tableExpressions) { + return addSynchronizedQuerySpace( tableExpressions ); + } + /** * 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. @@ -48,6 +76,18 @@ public interface SynchronizeableQuery { */ SynchronizeableQuery addSynchronizedEntityName(String entityName) throws MappingException; + /** + * Adds one-or-more entities (by name) whose tables should be added as synchronized spaces + */ + default SynchronizeableQuery 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 * {@link #addSynchronizedQuerySpace} for all tables associated with the given entity. @@ -58,5 +98,18 @@ public interface SynchronizeableQuery { * * @throws MappingException Indicates the given class could not be resolved as an entity */ + @SuppressWarnings( "rawtypes" ) SynchronizeableQuery addSynchronizedEntityClass(Class entityClass) throws MappingException; + + /** + * Adds one-or-more entities (by class) whose tables should be added as synchronized spaces + */ + default SynchronizeableQuery addSynchronizedEntityClass(Class... entityClasses) throws MappingException { + if ( entityClasses != null ) { + for ( int i = 0; i < entityClasses.length; i++ ) { + addSynchronizedEntityClass( entityClasses[i] ); + } + } + return this; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/NamedNativeQuery.java b/hibernate-core/src/main/java/org/hibernate/annotations/NamedNativeQuery.java index 8d495c63ec82..6fb65dd5c942 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/NamedNativeQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/NamedNativeQuery.java @@ -89,4 +89,11 @@ * Whether the results should be read-only. Default is {@code false}. */ boolean readOnly() default false; + + /** + * The query spaces to apply for the query. + * + * @see org.hibernate.SynchronizeableQuery + */ + String[] querySpaces() default {}; } diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/QueryHints.java b/hibernate-core/src/main/java/org/hibernate/annotations/QueryHints.java index b524bfe5bfcc..7bfd3ec0129a 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/QueryHints.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/QueryHints.java @@ -144,4 +144,17 @@ private QueryHints() { */ 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:
    + *
  • List of the spaces
  • + *
  • array of the spaces
  • + *
  • String "whitespace"-separated list of the spaces
  • + *
+ * + * @see org.hibernate.SynchronizeableQuery + */ + public static final String NATIVE_SPACES = "org.hibernate.query.native.spaces"; + } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java index 49489c9a0877..7a5dfd71e0b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AnnotationBinder.java @@ -364,59 +364,67 @@ private static void bindGenericGenerator(GenericGenerator def, MetadataBuildingC context.getMetadataCollector().addIdentifierGenerator( buildIdGenerator( def, context ) ); } - private static void bindQueries(XAnnotatedElement annotatedElement, MetadataBuildingContext context) { - { - SqlResultSetMapping ann = annotatedElement.getAnnotation( SqlResultSetMapping.class ); - QueryBinder.bindSqlResultSetMapping( ann, context, false ); - } - { - SqlResultSetMappings ann = annotatedElement.getAnnotation( SqlResultSetMappings.class ); - if ( ann != null ) { - for ( SqlResultSetMapping current : ann.value() ) { - QueryBinder.bindSqlResultSetMapping( current, context, false ); - } + private static void bindNamedJpaQueries(XAnnotatedElement annotatedElement, MetadataBuildingContext context) { + QueryBinder.bindSqlResultSetMapping( + annotatedElement.getAnnotation( SqlResultSetMapping.class ), + context, + false + ); + + final SqlResultSetMappings ann = annotatedElement.getAnnotation( SqlResultSetMappings.class ); + if ( ann != null ) { + for ( SqlResultSetMapping current : ann.value() ) { + QueryBinder.bindSqlResultSetMapping( current, context, false ); } } - { - NamedQuery ann = annotatedElement.getAnnotation( NamedQuery.class ); - QueryBinder.bindQuery( ann, context, false ); - } - { - org.hibernate.annotations.NamedQuery ann = annotatedElement.getAnnotation( - org.hibernate.annotations.NamedQuery.class - ); - QueryBinder.bindQuery( ann, context ); - } - { - NamedQueries ann = annotatedElement.getAnnotation( NamedQueries.class ); - QueryBinder.bindQueries( ann, context, false ); - } - { - org.hibernate.annotations.NamedQueries ann = annotatedElement.getAnnotation( - org.hibernate.annotations.NamedQueries.class - ); - QueryBinder.bindQueries( ann, context ); - } - { - NamedNativeQuery ann = annotatedElement.getAnnotation( NamedNativeQuery.class ); - QueryBinder.bindNativeQuery( ann, context, false ); - } - { - org.hibernate.annotations.NamedNativeQuery ann = annotatedElement.getAnnotation( - org.hibernate.annotations.NamedNativeQuery.class - ); - QueryBinder.bindNativeQuery( ann, context ); - } - { - NamedNativeQueries ann = annotatedElement.getAnnotation( NamedNativeQueries.class ); - QueryBinder.bindNativeQueries( ann, context, false ); - } - { - org.hibernate.annotations.NamedNativeQueries ann = annotatedElement.getAnnotation( - org.hibernate.annotations.NamedNativeQueries.class - ); - QueryBinder.bindNativeQueries( ann, context ); - } + + QueryBinder.bindQuery( + annotatedElement.getAnnotation( NamedQuery.class ), + context, + false + ); + + QueryBinder.bindQueries( + annotatedElement.getAnnotation( NamedQueries.class ), + context, + false + ); + + QueryBinder.bindNativeQuery( + annotatedElement.getAnnotation( NamedNativeQuery.class ), + context, + false + ); + + QueryBinder.bindNativeQueries( + annotatedElement.getAnnotation( NamedNativeQueries.class ), + context, + false + ); + } + + 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ bindNamedStoredProcedureQuery( diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/QueryBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/QueryBinder.java index c330d4aed601..a302fc5eb14b 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/QueryBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/QueryBinder.java @@ -48,11 +48,15 @@ public static void bindQuery( NamedQuery queryAnn, MetadataBuildingContext context, boolean isDefault) { - if ( queryAnn == null ) return; + if ( queryAnn == null ) { + return; + } + if ( BinderHelper.isEmptyAnnotationValue( queryAnn.name() ) ) { 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() ); String queryName = queryAnn.query(); NamedQueryDefinition queryDefinition = new NamedQueryDefinitionBuilder( queryAnn.name() ) @@ -114,14 +118,17 @@ public static void bindNativeQuery( if ( !BinderHelper.isEmptyAnnotationValue( resultSetMapping ) ) { //sql result set usage - builder.setResultSetRef( resultSetMapping ) - .createNamedQueryDefinition(); + builder.setResultSetRef( resultSetMapping ).createNamedQueryDefinition(); } else if ( !void.class.equals( queryAnn.resultClass() ) ) { //class mapping usage //FIXME should be done in a second pass due to entity name? - final NativeSQLQueryRootReturn entityQueryReturn = - new NativeSQLQueryRootReturn( "alias1", queryAnn.resultClass().getName(), new HashMap(), LockMode.READ ); + final NativeSQLQueryRootReturn entityQueryReturn = new NativeSQLQueryRootReturn( + "alias1", + queryAnn.resultClass().getName(), + new HashMap(), + LockMode.READ + ); builder.setQueryReturns( new NativeSQLQueryReturn[] {entityQueryReturn} ); } else { @@ -153,59 +160,50 @@ public static void bindNativeQuery( throw new AnnotationException( "A named query must have a name when used in class or package level" ); } - NamedSQLQueryDefinition query; - String resultSetMapping = queryAnn.resultSetMapping(); + final String resultSetMapping = queryAnn.resultSetMapping(); + + final NamedSQLQueryDefinitionBuilder builder = new NamedSQLQueryDefinitionBuilder() + .setName( queryAnn.name() ) + .setQuery( queryAnn.query() ) + .setCacheable( queryAnn.cacheable() ) + .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() ); + + if ( !BinderHelper.isEmptyAnnotationValue( resultSetMapping ) ) { //sql result set usage - query = new NamedSQLQueryDefinitionBuilder().setName( queryAnn.name() ) - .setQuery( queryAnn.query() ) - .setResultSetRef( resultSetMapping ) - .setQuerySpaces( null ) - .setCacheable( queryAnn.cacheable() ) - .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(); + builder.setResultSetRef( resultSetMapping ); } - else if ( !void.class.equals( queryAnn.resultClass() ) ) { + else if ( ! void.class.equals( queryAnn.resultClass() ) ) { //class mapping usage //FIXME should be done in a second pass due to entity name? - final NativeSQLQueryRootReturn entityQueryReturn = - new NativeSQLQueryRootReturn( "alias1", queryAnn.resultClass().getName(), new HashMap(), LockMode.READ ); - query = new NamedSQLQueryDefinitionBuilder().setName( queryAnn.name() ) - .setQuery( queryAnn.query() ) - .setQueryReturns( new NativeSQLQueryReturn[] {entityQueryReturn} ) - .setQuerySpaces( null ) - .setCacheable( queryAnn.cacheable() ) - .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(); + final NativeSQLQueryRootReturn entityQueryReturn = new NativeSQLQueryRootReturn( + "alias1", + queryAnn.resultClass().getName(), + new HashMap(), + LockMode.READ + ); + builder.setQueryReturns( new NativeSQLQueryReturn[] {entityQueryReturn} ); } 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 ); + if ( LOG.isDebugEnabled() ) { LOG.debugf( "Binding named native query: %s => %s", query.getName(), queryAnn.query() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/QueryHints.java b/hibernate-core/src/main/java/org/hibernate/jpa/QueryHints.java index d444bb031a15..b71d4cdf359c 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/QueryHints.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/QueryHints.java @@ -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.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.READ_ONLY; import static org.hibernate.annotations.QueryHints.TIMEOUT_HIBERNATE; @@ -26,8 +27,6 @@ /** * Defines the supported JPA query hints - * - * @author Steve Ebersole */ public class QueryHints { /** @@ -91,30 +90,27 @@ public class QueryHints { * * Note: Currently, attributes that are not specified are treated as FetchType.LAZY or FetchType.EAGER depending * on the attribute's definition in metadata, rather than forcing FetchType.LAZY. - * - * @deprecated (since 5.4) Use {@link GraphSemantic#FETCH}'s {@link GraphSemantic#getJpaHintName()} instead */ - @Deprecated public static final String HINT_FETCHGRAPH = GraphSemantic.FETCH.getJpaHintName(); /** * Hint providing a "loadgraph" EntityGraph. Attributes explicitly specified as AttributeNodes are treated as * FetchType.EAGER (via join fetch or subsequent select). Attributes that are not specified are treated as * FetchType.LAZY or FetchType.EAGER depending on the attribute's definition in metadata - * - * @deprecated (since 5.4) Use {@link GraphSemantic#LOAD}'s {@link GraphSemantic#getJpaHintName()} instead */ - @Deprecated public static final String HINT_LOADGRAPH = GraphSemantic.LOAD.getJpaHintName(); public static final String HINT_FOLLOW_ON_LOCKING = FOLLOW_ON_LOCKING; public static final String HINT_PASS_DISTINCT_THROUGH = PASS_DISTINCT_THROUGH; + public static final String HINT_NATIVE_SPACES = NATIVE_SPACES; + + private static final Set HINTS = buildHintsSet(); private static Set buildHintsSet() { - HashSet hints = new HashSet(); + HashSet hints = new HashSet<>(); hints.add( HINT_TIMEOUT ); hints.add( SPEC_HINT_TIMEOUT ); hints.add( HINT_COMMENT ); @@ -127,6 +123,7 @@ private static Set buildHintsSet() { hints.add( HINT_NATIVE_LOCKMODE ); hints.add( HINT_FETCHGRAPH ); hints.add( HINT_LOADGRAPH ); + hints.add( HINT_NATIVE_SPACES ); return java.util.Collections.unmodifiableSet( hints ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/AbstractProducedQuery.java b/hibernate-core/src/main/java/org/hibernate/query/internal/AbstractProducedQuery.java index a6eab2372c54..87a28dfbeacc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/AbstractProducedQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/AbstractProducedQuery.java @@ -27,9 +27,7 @@ import java.util.Spliterators; import java.util.function.BiConsumer; import java.util.function.BiFunction; -import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.Supplier; import java.util.stream.Stream; import java.util.stream.StreamSupport; import javax.persistence.CacheRetrieveMode; @@ -106,6 +104,7 @@ 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_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_TIMEOUT; import static org.hibernate.jpa.QueryHints.SPEC_HINT_TIMEOUT; @@ -1110,6 +1109,9 @@ else if ( JPA_SHARED_CACHE_STORE_MODE.equals( hintName ) ) { final CacheStoreMode storeMode = value != null ? CacheStoreMode.valueOf( value.toString() ) : null; applied = applyJpaCacheStoreMode( storeMode ); } + else if ( HINT_NATIVE_SPACES.equals( hintName ) ) { + applied = applyQuerySpaces( value ); + } else if ( QueryHints.HINT_NATIVE_LOCKMODE.equals( hintName ) ) { applied = applyNativeQueryLockMode( value ); } @@ -1162,6 +1164,12 @@ else if ( QueryHints.HINT_PASS_DISTINCT_THROUGH.equals( hintName ) ) { 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) { MSG_LOGGER.debugf( "Skipping unsupported query hint [%s]", hintName ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/NativeQueryImpl.java index be55e1715456..abe7ac5686dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/NativeQueryImpl.java @@ -19,6 +19,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.StringTokenizer; import javax.persistence.FlushModeType; import javax.persistence.LockModeType; import javax.persistence.Parameter; @@ -32,6 +33,7 @@ import org.hibernate.MappingException; import org.hibernate.QueryException; import org.hibernate.ScrollMode; +import org.hibernate.SynchronizeableQuery; import org.hibernate.engine.ResultSetMappingDefinition; import org.hibernate.engine.query.spi.EntityGraphQueryHint; import org.hibernate.engine.query.spi.sql.NativeSQLQueryConstructorReturn; @@ -44,6 +46,7 @@ import org.hibernate.graph.GraphSemantic; import org.hibernate.graph.RootGraph; import org.hibernate.internal.util.StringHelper; +import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.query.NativeQuery; import org.hibernate.query.ParameterMetadata; import org.hibernate.query.Query; @@ -249,7 +252,7 @@ else if ( NativeSQLQueryConstructorReturn.class.isInstance( queryReturn ) ) { 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 // perform a partial flush according to the defined query spaces, no need to do a full flush. return; @@ -441,12 +444,18 @@ public NativeQueryImplementor addSynchronizedQuerySpace(String querySpace) { return this; } + @Override + public SynchronizeableQuery addSynchronizedQuerySpace(String... querySpaces) { + addQuerySpaces( querySpaces ); + return this; + } + protected void addQuerySpaces(String... spaces) { if ( spaces != null ) { if ( querySpaces == null ) { querySpaces = new ArrayList<>(); } - querySpaces.addAll( Arrays.asList( (String[]) spaces ) ); + querySpaces.addAll( Arrays.asList( spaces ) ); } } @@ -471,6 +480,36 @@ public NativeQueryImplementor addSynchronizedEntityClass(Class entityClass) t 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) 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 protected boolean isNativeQuery() { return true; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/SynchronizedSpaceTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/SynchronizedSpaceTests.java new file mode 100644 index 000000000000..800220092b4a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/SynchronizedSpaceTests.java @@ -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 . + */ +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 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 queryProducer, + Consumer 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 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; + } + } +}