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;
import java.util.List;
import javax.persistence.QueryHint;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Order;
@ -505,6 +507,18 @@ public interface Criteria extends CriteriaSpecification {
* @return this (for method chaining)
*/
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.

View File

@ -34,6 +34,8 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.persistence.QueryHint;
import org.hibernate.transform.ResultTransformer;
import org.hibernate.type.Type;
@ -211,6 +213,16 @@ public interface Query extends BasicQueryContract {
* @see #getComment()
*/
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.

View File

@ -35,6 +35,7 @@ import java.sql.Types;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
@ -2658,4 +2659,16 @@ public abstract class Dialect implements ConversionContext {
public boolean supportsNotNullUnique() {
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.SQLException;
import java.sql.Types;
import java.util.List;
import org.hibernate.JDBCException;
import org.hibernate.QueryTimeoutException;
import org.hibernate.annotations.common.util.StringHelper;
import org.hibernate.cfg.Environment;
import org.hibernate.dialect.function.NoArgSQLFunction;
import org.hibernate.dialect.function.NvlFunction;
@ -582,4 +584,26 @@ public class Oracle8iDialect extends Dialect {
public String getNotExpression( String 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 String cacheRegion;
private String comment;
private List<String> queryHints;
private ScrollMode scrollMode;
private Serializable[] collectionKeys;
private Object optionalObject;
@ -101,7 +102,7 @@ public final class QueryParameters {
public QueryParameters(
final Type[] positionalParameterTypes,
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(
@ -127,6 +128,7 @@ public final class QueryParameters {
false,
null,
null,
null,
collectionKeys,
null
);
@ -143,6 +145,7 @@ public final class QueryParameters {
final String cacheRegion,
//final boolean forceCacheRefresh,
final String comment,
final List<String> queryHints,
final boolean isLookupByNaturalKey,
final ResultTransformer transformer) {
this(
@ -156,6 +159,7 @@ public final class QueryParameters {
cacheable,
cacheRegion,
comment,
queryHints,
null,
transformer
);
@ -174,6 +178,7 @@ public final class QueryParameters {
final String cacheRegion,
//final boolean forceCacheRefresh,
final String comment,
final List<String> queryHints,
final Serializable[] collectionKeys,
ResultTransformer transformer) {
this.positionalParameterTypes = positionalParameterTypes;
@ -185,6 +190,7 @@ public final class QueryParameters {
this.cacheRegion = cacheRegion;
//this.forceCacheRefresh = forceCacheRefresh;
this.comment = comment;
this.queryHints = queryHints;
this.collectionKeys = collectionKeys;
this.isReadOnlyInitialized = isReadOnlyInitialized;
this.readOnly = readOnly;
@ -203,6 +209,7 @@ public final class QueryParameters {
final String cacheRegion,
//final boolean forceCacheRefresh,
final String comment,
final List<String> queryHints,
final Serializable[] collectionKeys,
final Object optionalObject,
final String optionalEntityName,
@ -219,6 +226,7 @@ public final class QueryParameters {
cacheable,
cacheRegion,
comment,
queryHints,
collectionKeys,
transformer
);
@ -322,6 +330,14 @@ public final class QueryParameters {
public void setComment(String comment) {
this.comment = comment;
}
public List<String> getQueryHints() {
return queryHints;
}
public void setQueryHints(List<String> queryHints) {
this.queryHints = queryHints;
}
public ScrollMode getScrollMode() {
return scrollMode;
@ -561,6 +577,7 @@ public final class QueryParameters {
this.cacheable,
this.cacheRegion,
this.comment,
this.queryHints,
this.collectionKeys,
this.optionalObject,
this.optionalEntityName,

View File

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

View File

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

View File

@ -242,6 +242,13 @@ public abstract class Loader {
Dialect dialect,
List<AfterLoadAction> afterLoadActions) throws HibernateException {
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()
? prependComment( sql, parameters )
: sql;
@ -1844,7 +1851,7 @@ public abstract class Loader {
* limit parameters.
*/
protected final PreparedStatement prepareQueryStatement(
final String sql,
String sql,
final QueryParameters queryParameters,
final LimitHandler limitHandler,
final boolean scroll,
@ -1856,7 +1863,7 @@ public abstract class Loader {
boolean useLimitOffset = hasFirstRow && useLimit && limitHandler.supportsLimitOffset();
boolean callable = queryParameters.isCallable();
final ScrollMode scrollMode = getScrollMode( scroll, hasFirstRow, useLimitOffset, queryParameters );
PreparedStatement st = session.getTransactionCoordinator().getJdbcCoordinator().getStatementPreparer().prepareQueryStatement(
sql,
callable,

View File

@ -355,6 +355,7 @@ public class CriteriaQueryTranslator implements CriteriaQuery {
rootCriteria.getCacheable(),
rootCriteria.getCacheRegion(),
rootCriteria.getComment(),
rootCriteria.getQueryHints(),
rootCriteria.isLookupByNaturalKey(),
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;
}
}