HHH-2736 query hints for Query and Criteria

This commit is contained in:
Brett Meyer 2013-07-10 01:37:13 -04:00
parent 6beb5acb4b
commit 742b1b4156
10 changed files with 294 additions and 3 deletions

View File

@ -25,6 +25,8 @@
package org.hibernate; package org.hibernate;
import java.util.List; import java.util.List;
import javax.persistence.QueryHint;
import org.hibernate.criterion.CriteriaSpecification; import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Criterion; import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order; import org.hibernate.criterion.Order;
@ -505,6 +507,18 @@ public interface Criteria extends CriteriaSpecification {
* @return this (for method chaining) * @return this (for method chaining)
*/ */
public Criteria setComment(String comment); public Criteria setComment(String comment);
/**
* Add a DB query hint to the SQL. These differ from JPA's {@link QueryHint}, which is specific to the JPA
* implementation and ignores DB vendor-specific hints. Instead, these are intended solely for the vendor-specific
* hints, such as Oracle's optimizers. Multiple query hints are supported; the Dialect will determine
* concatenation and placement.
*
* @param hint The database specific query hint to add.
* @return this (for method chaining)
*/
public Criteria addQueryHint(String hint);
/** /**
* Override the flush mode for this particular query. * Override the flush mode for this particular query.

View File

@ -34,6 +34,8 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import javax.persistence.QueryHint;
import org.hibernate.transform.ResultTransformer; import org.hibernate.transform.ResultTransformer;
import org.hibernate.type.Type; import org.hibernate.type.Type;
@ -211,6 +213,16 @@ public interface Query extends BasicQueryContract {
* @see #getComment() * @see #getComment()
*/ */
public Query setComment(String comment); public Query setComment(String comment);
/**
* Add a DB query hint to the SQL. These differ from JPA's {@link QueryHint}, which is specific to the JPA
* implementation and ignores DB vendor-specific hints. Instead, these are intended solely for the vendor-specific
* hints, such as Oracle's optimizers. Multiple query hints are supported; the Dialect will determine
* concatenation and placement.
*
* @param hint The database specific query hint to add.
*/
public Query addQueryHint(String hint);
/** /**
* Return the HQL select clause aliases, if any. * Return the HQL select clause aliases, if any.

View File

@ -35,6 +35,7 @@ import java.sql.Types;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set; import java.util.Set;
@ -2658,4 +2659,16 @@ public abstract class Dialect implements ConversionContext {
public boolean supportsNotNullUnique() { public boolean supportsNotNullUnique() {
return true; return true;
} }
/**
* Apply a hint to the query. The entire query is provided, allowing the Dialect full control over the placement
* and syntax of the hint. By default, ignore the hint and simply return the query.
*
* @param query The query to which to apply the hint.
* @param hints The hints to apply
* @return The modified SQL
*/
public String getQueryHintString(String query, List<String> hints) {
return query;
}
} }

View File

@ -27,9 +27,11 @@ import java.sql.CallableStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types; import java.sql.Types;
import java.util.List;
import org.hibernate.JDBCException; import org.hibernate.JDBCException;
import org.hibernate.QueryTimeoutException; import org.hibernate.QueryTimeoutException;
import org.hibernate.annotations.common.util.StringHelper;
import org.hibernate.cfg.Environment; import org.hibernate.cfg.Environment;
import org.hibernate.dialect.function.NoArgSQLFunction; import org.hibernate.dialect.function.NoArgSQLFunction;
import org.hibernate.dialect.function.NvlFunction; import org.hibernate.dialect.function.NvlFunction;
@ -582,4 +584,26 @@ public class Oracle8iDialect extends Dialect {
public String getNotExpression( String expression ) { public String getNotExpression( String expression ) {
return "not (" + expression + ")"; return "not (" + expression + ")";
} }
@Override
public String getQueryHintString(String sql, List<String> hints) {
final String hint = StringHelper.join( ", ", hints.iterator() );
if ( StringHelper.isEmpty( hint ) ) {
return sql;
}
final int pos = sql.indexOf( "select" );
if ( pos > -1 ) {
final StringBuilder buffer = new StringBuilder( sql.length() + hint.length() + 8 );
if ( pos > 0 ) {
buffer.append( sql.substring( 0, pos ) );
}
buffer.append( "select /*+ " ).append( hint ).append( " */" )
.append( sql.substring( pos + "select".length() ) );
sql = buffer.toString();
}
return sql;
}
} }

View File

@ -60,6 +60,7 @@ public final class QueryParameters {
private boolean cacheable; private boolean cacheable;
private String cacheRegion; private String cacheRegion;
private String comment; private String comment;
private List<String> queryHints;
private ScrollMode scrollMode; private ScrollMode scrollMode;
private Serializable[] collectionKeys; private Serializable[] collectionKeys;
private Object optionalObject; private Object optionalObject;
@ -101,7 +102,7 @@ public final class QueryParameters {
public QueryParameters( public QueryParameters(
final Type[] positionalParameterTypes, final Type[] positionalParameterTypes,
final Object[] positionalParameterValues) { final Object[] positionalParameterValues) {
this( positionalParameterTypes, positionalParameterValues, null, null, false, false, false, null, null, false, null ); this( positionalParameterTypes, positionalParameterValues, null, null, false, false, false, null, null, null, false, null );
} }
public QueryParameters( public QueryParameters(
@ -127,6 +128,7 @@ public final class QueryParameters {
false, false,
null, null,
null, null,
null,
collectionKeys, collectionKeys,
null null
); );
@ -143,6 +145,7 @@ public final class QueryParameters {
final String cacheRegion, final String cacheRegion,
//final boolean forceCacheRefresh, //final boolean forceCacheRefresh,
final String comment, final String comment,
final List<String> queryHints,
final boolean isLookupByNaturalKey, final boolean isLookupByNaturalKey,
final ResultTransformer transformer) { final ResultTransformer transformer) {
this( this(
@ -156,6 +159,7 @@ public final class QueryParameters {
cacheable, cacheable,
cacheRegion, cacheRegion,
comment, comment,
queryHints,
null, null,
transformer transformer
); );
@ -174,6 +178,7 @@ public final class QueryParameters {
final String cacheRegion, final String cacheRegion,
//final boolean forceCacheRefresh, //final boolean forceCacheRefresh,
final String comment, final String comment,
final List<String> queryHints,
final Serializable[] collectionKeys, final Serializable[] collectionKeys,
ResultTransformer transformer) { ResultTransformer transformer) {
this.positionalParameterTypes = positionalParameterTypes; this.positionalParameterTypes = positionalParameterTypes;
@ -185,6 +190,7 @@ public final class QueryParameters {
this.cacheRegion = cacheRegion; this.cacheRegion = cacheRegion;
//this.forceCacheRefresh = forceCacheRefresh; //this.forceCacheRefresh = forceCacheRefresh;
this.comment = comment; this.comment = comment;
this.queryHints = queryHints;
this.collectionKeys = collectionKeys; this.collectionKeys = collectionKeys;
this.isReadOnlyInitialized = isReadOnlyInitialized; this.isReadOnlyInitialized = isReadOnlyInitialized;
this.readOnly = readOnly; this.readOnly = readOnly;
@ -203,6 +209,7 @@ public final class QueryParameters {
final String cacheRegion, final String cacheRegion,
//final boolean forceCacheRefresh, //final boolean forceCacheRefresh,
final String comment, final String comment,
final List<String> queryHints,
final Serializable[] collectionKeys, final Serializable[] collectionKeys,
final Object optionalObject, final Object optionalObject,
final String optionalEntityName, final String optionalEntityName,
@ -219,6 +226,7 @@ public final class QueryParameters {
cacheable, cacheable,
cacheRegion, cacheRegion,
comment, comment,
queryHints,
collectionKeys, collectionKeys,
transformer transformer
); );
@ -322,6 +330,14 @@ public final class QueryParameters {
public void setComment(String comment) { public void setComment(String comment) {
this.comment = comment; this.comment = comment;
} }
public List<String> getQueryHints() {
return queryHints;
}
public void setQueryHints(List<String> queryHints) {
this.queryHints = queryHints;
}
public ScrollMode getScrollMode() { public ScrollMode getScrollMode() {
return scrollMode; return scrollMode;
@ -561,6 +577,7 @@ public final class QueryParameters {
this.cacheable, this.cacheable,
this.cacheRegion, this.cacheRegion,
this.comment, this.comment,
this.queryHints,
this.collectionKeys, this.collectionKeys,
this.optionalObject, this.optionalObject,
this.optionalEntityName, this.optionalEntityName,

View File

@ -101,6 +101,7 @@ public abstract class AbstractQueryImpl implements Query {
private boolean cacheable; private boolean cacheable;
private String cacheRegion; private String cacheRegion;
private String comment; private String comment;
private final List<String> queryHints = new ArrayList<String>();
private FlushMode flushMode; private FlushMode flushMode;
private CacheMode cacheMode; private CacheMode cacheMode;
private FlushMode sessionFlushMode; private FlushMode sessionFlushMode;
@ -192,6 +193,12 @@ public abstract class AbstractQueryImpl implements Query {
this.comment = comment; this.comment = comment;
return this; return this;
} }
@Override
public Query addQueryHint(String queryHint) {
queryHints.add( queryHint );
return this;
}
@Override @Override
public Integer getFirstResult() { public Integer getFirstResult() {
@ -987,6 +994,7 @@ public abstract class AbstractQueryImpl implements Query {
cacheable, cacheable,
cacheRegion, cacheRegion,
comment, comment,
queryHints,
collectionKey == null ? null : new Serializable[] { collectionKey }, collectionKey == null ? null : new Serializable[] { collectionKey },
optionalObject, optionalObject,
optionalEntityName, optionalEntityName,

View File

@ -75,6 +75,7 @@ public class CriteriaImpl implements Criteria, Serializable {
private boolean cacheable; private boolean cacheable;
private String cacheRegion; private String cacheRegion;
private String comment; private String comment;
private final List<String> queryHints = new ArrayList<String>();
private FlushMode flushMode; private FlushMode flushMode;
private CacheMode cacheMode; private CacheMode cacheMode;
@ -345,11 +346,23 @@ public class CriteriaImpl implements Criteria, Serializable {
public String getComment() { public String getComment() {
return comment; return comment;
} }
@Override @Override
public Criteria setComment(String comment) { public Criteria setComment(String comment) {
this.comment = comment; this.comment = comment;
return this; return this;
} }
public List<String> getQueryHints() {
return queryHints;
}
@Override
public Criteria addQueryHint(String queryHint) {
queryHints.add( queryHint );
return this;
}
@Override @Override
public Criteria setFlushMode(FlushMode flushMode) { public Criteria setFlushMode(FlushMode flushMode) {
this.flushMode = flushMode; this.flushMode = flushMode;
@ -666,6 +679,11 @@ public class CriteriaImpl implements Criteria, Serializable {
public Criteria setComment(String comment) { public Criteria setComment(String comment) {
CriteriaImpl.this.setComment(comment); CriteriaImpl.this.setComment(comment);
return this; return this;
}
@Override
public Criteria addQueryHint(String queryHint) {
CriteriaImpl.this.addQueryHint( queryHint );
return this;
} }
@Override @Override
public Criteria setProjection(Projection projection) { public Criteria setProjection(Projection projection) {

View File

@ -242,6 +242,13 @@ public abstract class Loader {
Dialect dialect, Dialect dialect,
List<AfterLoadAction> afterLoadActions) throws HibernateException { List<AfterLoadAction> afterLoadActions) throws HibernateException {
sql = applyLocks( sql, parameters, dialect, afterLoadActions ); sql = applyLocks( sql, parameters, dialect, afterLoadActions );
// Keep this here, rather than moving to Select. Some Dialects may need the hint to be appended to the very
// end or beginning of the finalized SQL statement, so wait until everything is processed.
if ( parameters.getQueryHints() != null && parameters.getQueryHints().size() > 0 ) {
sql = dialect.getQueryHintString( sql, parameters.getQueryHints() );
}
return getFactory().getSettings().isCommentsEnabled() return getFactory().getSettings().isCommentsEnabled()
? prependComment( sql, parameters ) ? prependComment( sql, parameters )
: sql; : sql;
@ -1844,7 +1851,7 @@ public abstract class Loader {
* limit parameters. * limit parameters.
*/ */
protected final PreparedStatement prepareQueryStatement( protected final PreparedStatement prepareQueryStatement(
final String sql, String sql,
final QueryParameters queryParameters, final QueryParameters queryParameters,
final LimitHandler limitHandler, final LimitHandler limitHandler,
final boolean scroll, final boolean scroll,
@ -1856,7 +1863,7 @@ public abstract class Loader {
boolean useLimitOffset = hasFirstRow && useLimit && limitHandler.supportsLimitOffset(); boolean useLimitOffset = hasFirstRow && useLimit && limitHandler.supportsLimitOffset();
boolean callable = queryParameters.isCallable(); boolean callable = queryParameters.isCallable();
final ScrollMode scrollMode = getScrollMode( scroll, hasFirstRow, useLimitOffset, queryParameters ); final ScrollMode scrollMode = getScrollMode( scroll, hasFirstRow, useLimitOffset, queryParameters );
PreparedStatement st = session.getTransactionCoordinator().getJdbcCoordinator().getStatementPreparer().prepareQueryStatement( PreparedStatement st = session.getTransactionCoordinator().getJdbcCoordinator().getStatementPreparer().prepareQueryStatement(
sql, sql,
callable, callable,

View File

@ -355,6 +355,7 @@ public class CriteriaQueryTranslator implements CriteriaQuery {
rootCriteria.getCacheable(), rootCriteria.getCacheable(),
rootCriteria.getCacheRegion(), rootCriteria.getCacheRegion(),
rootCriteria.getComment(), rootCriteria.getComment(),
rootCriteria.getQueryHints(),
rootCriteria.isLookupByNaturalKey(), rootCriteria.isLookupByNaturalKey(),
rootCriteria.getResultTransformer() rootCriteria.getResultTransformer()
); );

View File

@ -0,0 +1,177 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* JBoss, Home of Professional Open Source
* Copyright 2013 Red Hat Inc. and/or its affiliates and other contributors
* as indicated by the @authors tag. All rights reserved.
* See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This copyrighted material is made available to anyone wishing to use,
* modify, copy, or redistribute it subject to the terms and conditions
* of the GNU Lesser General Public License, v. 2.1.
* This program is distributed in the hope that it will be useful, but WITHOUT A
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General Public License,
* v.2.1 along with this distribution; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301, USA.
*/
package org.hibernate.test.queryhint;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.criterion.Restrictions;
import org.hibernate.dialect.Oracle8iDialect;
import org.hibernate.testing.RequiresDialect;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.junit.Test;
/**
* @author Brett Meyer
*/
@RequiresDialect( Oracle8iDialect.class )
public class QueryHintTest extends BaseCoreFunctionalTestCase {
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { Employee.class, Department.class };
}
@Override
protected void configure(Configuration configuration) {
configuration.setProperty( AvailableSettings.DIALECT, QueryHintTestDialect.class.getName() );
configuration.setProperty( AvailableSettings.USE_SQL_COMMENTS, "true" );
}
@Test
public void testQueryHint() {
Department department = new Department();
department.name = "Sales";
Employee employee1 = new Employee();
employee1.department = department;
Employee employee2 = new Employee();
employee2.department = department;
Session s = openSession();
s.getTransaction().begin();
s.persist( department ); s.persist( employee1 );
s.persist( employee2 );
s.getTransaction().commit();
s.clear();
// test Query w/ a simple Oracle optimizer hint
s.getTransaction().begin();
Query query = s.createQuery( "FROM QueryHintTest$Employee e WHERE e.department.name = :departmentName" )
.addQueryHint( "ALL_ROWS" )
.setParameter( "departmentName", "Sales" );
List results = query.list();
s.getTransaction().commit();
s.clear();
assertEquals(results.size(), 2);
assertTrue(QueryHintTestDialect.getProcessedSql().contains( "select /*+ ALL_ROWS */"));
QueryHintTestDialect.resetProcessedSql();
// test multiple hints
s.getTransaction().begin();
query = s.createQuery( "FROM QueryHintTest$Employee e WHERE e.department.name = :departmentName" )
.addQueryHint( "ALL_ROWS" )
.addQueryHint( "USE_CONCAT" )
.setParameter( "departmentName", "Sales" );
results = query.list();
s.getTransaction().commit();
s.clear();
assertEquals(results.size(), 2);
assertTrue(QueryHintTestDialect.getProcessedSql().contains( "select /*+ ALL_ROWS, USE_CONCAT */"));
QueryHintTestDialect.resetProcessedSql();
// ensure the insertion logic can handle a comment appended to the front
s.getTransaction().begin();
query = s.createQuery( "FROM QueryHintTest$Employee e WHERE e.department.name = :departmentName" )
.setComment( "this is a test" )
.addQueryHint( "ALL_ROWS" )
.setParameter( "departmentName", "Sales" );
results = query.list();
s.getTransaction().commit();
s.clear();
assertEquals(results.size(), 2);
assertTrue(QueryHintTestDialect.getProcessedSql().contains( "select /*+ ALL_ROWS */"));
QueryHintTestDialect.resetProcessedSql();
// test Criteria
s.getTransaction().begin();
Criteria criteria = s.createCriteria( Employee.class )
.addQueryHint( "ALL_ROWS" )
.createCriteria( "department" ).add( Restrictions.eq( "name", "Sales" ) );
results = criteria.list();
s.getTransaction().commit();
s.close();
assertEquals(results.size(), 2);
assertTrue(QueryHintTestDialect.getProcessedSql().contains( "select /*+ ALL_ROWS */"));
}
/**
* Since the query hint is added to the SQL during Loader's executeQueryStatement -> preprocessSQL, rather than
* early on during the QueryTranslator or QueryLoader initialization, there's not an easy way to check the full SQL
* after completely processing it. Instead, use this ridiculous hack to ensure Loader actually calls Dialect.
*
* TODO: This is terrible. Better ideas?
*/
public static class QueryHintTestDialect extends Oracle8iDialect {
private static String processedSql;
@Override
public String getQueryHintString(String sql, List<String> hints) {
processedSql = super.getQueryHintString( sql, hints );
return processedSql;
}
public static String getProcessedSql() {
return processedSql;
}
public static void resetProcessedSql() {
processedSql = "";
}
}
@Entity
public static class Employee {
@Id
@GeneratedValue
public long id;
@ManyToOne
public Department department;
}
@Entity
public static class Department {
@Id
@GeneratedValue
public long id;
public String name;
}
}