HHH-14325 - Add Query hint for specifying "query spaces" for native queries
This commit is contained in:
parent
f9cce5a767
commit
37a8d22155
|
@ -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
|
||||
* 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<T> {
|
||||
/**
|
||||
* Obtain the list of query spaces the query is synchronized on.
|
||||
|
@ -36,6 +38,18 @@ public interface SynchronizeableQuery<T> {
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -43,6 +57,13 @@ public interface SynchronizeableQuery<T> {
|
|||
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
|
||||
* {@link #addSynchronizedQuerySpace} for all tables associated with the given entity.
|
||||
|
@ -55,6 +76,18 @@ public interface SynchronizeableQuery<T> {
|
|||
*/
|
||||
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
|
||||
* {@link #addSynchronizedQuerySpace} for all tables associated with the given entity.
|
||||
|
@ -65,5 +98,18 @@ public interface SynchronizeableQuery<T> {
|
|||
*
|
||||
* @throws MappingException Indicates the given class could not be resolved as an entity
|
||||
*/
|
||||
@SuppressWarnings( "rawtypes" )
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,4 +89,11 @@ public @interface NamedNativeQuery {
|
|||
* 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 {};
|
||||
}
|
||||
|
|
|
@ -144,4 +144,17 @@ public class 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:<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";
|
||||
|
||||
}
|
||||
|
|
|
@ -364,59 +364,67 @@ public final class AnnotationBinder {
|
|||
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(
|
||||
|
|
|
@ -48,11 +48,15 @@ public abstract class QueryBinder {
|
|||
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 abstract class QueryBinder {
|
|||
|
||||
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 abstract class QueryBinder {
|
|||
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() );
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import static org.hibernate.annotations.QueryHints.FETCH_SIZE;
|
|||
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 @@ import static org.hibernate.annotations.QueryHints.TIMEOUT_JPA;
|
|||
|
||||
/**
|
||||
* 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<String> HINTS = buildHintsSet();
|
||||
|
||||
private static Set<String> buildHintsSet() {
|
||||
HashSet<String> hints = new HashSet<String>();
|
||||
HashSet<String> hints = new HashSet<>();
|
||||
hints.add( HINT_TIMEOUT );
|
||||
hints.add( SPEC_HINT_TIMEOUT );
|
||||
hints.add( HINT_COMMENT );
|
||||
|
@ -127,6 +123,7 @@ public class QueryHints {
|
|||
hints.add( HINT_NATIVE_LOCKMODE );
|
||||
hints.add( HINT_FETCHGRAPH );
|
||||
hints.add( HINT_LOADGRAPH );
|
||||
hints.add( HINT_NATIVE_SPACES );
|
||||
return java.util.Collections.unmodifiableSet( hints );
|
||||
}
|
||||
|
||||
|
|
|
@ -27,9 +27,7 @@ import java.util.Spliterator;
|
|||
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_FETCH_SIZE;
|
|||
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 @@ public abstract class AbstractProducedQuery<R> implements QueryImplementor<R> {
|
|||
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 @@ public abstract class AbstractProducedQuery<R> implements QueryImplementor<R> {
|
|||
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 );
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import java.util.Date;
|
|||
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.LockOptions;
|
|||
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.engine.spi.SharedSessionContractImplementor;
|
|||
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 @@ public class NativeQueryImpl<T> extends AbstractProducedQuery<T> implements Nati
|
|||
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 class NativeQueryImpl<T> extends AbstractProducedQuery<T> implements Nati
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SynchronizeableQuery<T> 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 class NativeQueryImpl<T> extends AbstractProducedQuery<T> implements Nati
|
|||
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
|
||||
protected boolean isNativeQuery() {
|
||||
return true;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue