diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 029bdbbc26..464b24c4f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -2788,6 +2788,24 @@ public abstract class Dialect implements ConversionContext { 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 hintList The hints to apply + * @return The modified SQL + */ + public String getQueryHintString(String query, List hintList) { + final String hints = StringHelper.join( ", ", hintList.iterator() ); + + if ( StringHelper.isEmpty( hints ) ) { + return query; + } + + return getQueryHintString( query, hints ); + } + /** * 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. @@ -2796,7 +2814,7 @@ public abstract class Dialect implements ConversionContext { * @param hints The hints to apply * @return The modified SQL */ - public String getQueryHintString(String query, List hints) { + public String getQueryHintString(String query, String hints) { return query; } @@ -2949,4 +2967,32 @@ public abstract class Dialect implements ConversionContext { false ); } + + /** + * Modify the SQL, adding hints or comments, if necessary + * + * @param sql original sql + * @param parameters query parameters + * @param commentsEnabled if comments are enabled + */ + public String addSqlHintOrComment( + String sql, + QueryParameters parameters, + boolean commentsEnabled) { + + // 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 = getQueryHintString( sql, parameters.getQueryHints() ); + } + else if ( commentsEnabled && parameters.getComment() != null ){ + sql = prependComment( sql, parameters.getComment() ); + } + + return sql; + } + + protected String prependComment(String sql, String comment) { + return "/* " + comment + " */ " + sql; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 486b2fede7..fea16552b5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -17,6 +17,7 @@ import org.hibernate.dialect.function.AvgWithArgumentCastFunction; import org.hibernate.dialect.function.NoArgSQLFunction; import org.hibernate.dialect.function.StandardSQLFunction; import org.hibernate.dialect.function.VarArgsSQLFunction; +import org.hibernate.dialect.hint.IndexQueryHintHandler; import org.hibernate.dialect.identity.H2IdentityColumnSupport; import org.hibernate.dialect.identity.IdentityColumnSupport; import org.hibernate.dialect.pagination.AbstractLimitHandler; @@ -441,4 +442,9 @@ public class H2Dialect extends Dialect { public IdentityColumnSupport getIdentityColumnSupport() { return new H2IdentityColumnSupport(); } + + @Override + public String getQueryHintString(String query, String hints) { + return IndexQueryHintHandler.INSTANCE.addQueryHints( query, hints ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQL5Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQL5Dialect.java index b8bd7e4aaf..c58320cf33 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQL5Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQL5Dialect.java @@ -9,6 +9,7 @@ package org.hibernate.dialect; import java.sql.SQLException; import java.sql.Types; +import org.hibernate.dialect.hint.IndexQueryHintHandler; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtracter; import org.hibernate.exception.spi.ViolatedConstraintNameExtracter; import org.hibernate.internal.util.JdbcExceptionHelper; @@ -54,4 +55,9 @@ public class MySQL5Dialect extends MySQLDialect { } }; + + @Override + public String getQueryHintString(String query, String hints) { + return IndexQueryHintHandler.INSTANCE.addQueryHints( query, hints ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Oracle8iDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Oracle8iDialect.java index 65ed7f7a1a..0d79399dd7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Oracle8iDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Oracle8iDialect.java @@ -12,6 +12,7 @@ import java.sql.SQLException; import java.sql.Types; import java.util.List; import java.util.Locale; +import java.util.regex.Matcher; import java.util.regex.Pattern; import org.hibernate.JDBCException; @@ -65,6 +66,8 @@ public class Oracle8iDialect extends Dialect { private static final Pattern UNION_KEYWORD_PATTERN = Pattern.compile( "\\bunion\\b" ); + private static final Pattern SQL_STATEMENT_TYPE_PATTERN = Pattern.compile("^\\s*(select|insert|update|delete)\\s+.*?"); + private static final AbstractLimitHandler LIMIT_HANDLER = new AbstractLimitHandler() { @Override public String processSql(String sql, RowSelection selection) { @@ -669,21 +672,21 @@ public class Oracle8iDialect extends Dialect { } @Override - public String getQueryHintString(String sql, List hints) { - final String hint = StringHelper.join( ", ", hints.iterator() ); - - if ( StringHelper.isEmpty( hint ) ) { - return sql; - } + public String getQueryHintString(String sql, String hints) { + String statementType = statementType(sql); - final int pos = sql.indexOf( "select" ); + final int pos = sql.indexOf( statementType ); if ( pos > -1 ) { - final StringBuilder buffer = new StringBuilder( sql.length() + hint.length() + 8 ); + final StringBuilder buffer = new StringBuilder( sql.length() + hints.length() + 8 ); if ( pos > 0 ) { buffer.append( sql.substring( 0, pos ) ); } - buffer.append( "select /*+ " ).append( hint ).append( " */" ) - .append( sql.substring( pos + "select".length() ) ); + buffer + .append( statementType ) + .append( " /*+ " ) + .append( hints ) + .append( " */" ) + .append( sql.substring( pos + statementType.length() ) ); sql = buffer.toString(); } @@ -711,4 +714,14 @@ public class Oracle8iDialect extends Dialect { public boolean supportsPartitionBy() { return true; } + + protected String statementType(String sql) { + Matcher matcher = SQL_STATEMENT_TYPE_PATTERN.matcher( sql ); + + if(matcher.matches() && matcher.groupCount() == 1) { + return matcher.group(1); + } + + throw new IllegalArgumentException( "Can't determine SQL statement type for statement: " + sql ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2012Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2012Dialect.java index 64a303e3d9..ebc074d680 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2012Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServer2012Dialect.java @@ -55,16 +55,10 @@ public class SQLServer2012Dialect extends SQLServer2008Dialect { } @Override - public String getQueryHintString(String sql, List hints) { - final String hint = StringHelper.join( ", ", hints.iterator() ); - - if ( StringHelper.isEmpty( hint ) ) { - return sql; - } - + public String getQueryHintString(String sql, String hints) { final StringBuilder buffer = new StringBuilder( sql.length() - + hint.length() + 12 + + hints.length() + 12 ); final int pos = sql.indexOf( ";" ); if ( pos > -1 ) { @@ -73,7 +67,7 @@ public class SQLServer2012Dialect extends SQLServer2008Dialect { else { buffer.append( sql ); } - buffer.append( " OPTION (" ).append( hint ).append( ")" ); + buffer.append( " OPTION (" ).append( hints ).append( ")" ); if ( pos > -1 ) { buffer.append( ";" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java b/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java new file mode 100644 index 0000000000..36ce81083a --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java @@ -0,0 +1,48 @@ +/* + * 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.dialect.hint; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Adds an INDEX query hint as follows: + * + * + * SELECT * + * FROM TEST + * USE INDEX (hint1, hint2) + * WHERE X=1 + * + * + * @author Vlad Mihalcea + */ +public class IndexQueryHintHandler implements QueryHintHandler { + + public static final IndexQueryHintHandler INSTANCE = new IndexQueryHintHandler(); + + private static final Pattern QUERY_PATTERN = Pattern.compile( "^(select.*?from.*?)(where.*?)$" ); + + @Override + public String addQueryHints(String query, String hints) { + Matcher matcher = QUERY_PATTERN.matcher( query ); + if ( matcher.matches() && matcher.groupCount() > 1 ) { + String startToken = matcher.group( 1 ); + String endToken = matcher.group( 2 ); + + return new StringBuilder( startToken ) + .append( " USE INDEX (" ) + .append( hints ) + .append( ") " ) + .append( endToken ) + .toString(); + } + else { + return query; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/hint/QueryHintHandler.java b/hibernate-core/src/main/java/org/hibernate/dialect/hint/QueryHintHandler.java new file mode 100644 index 0000000000..4067fcdad9 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/hint/QueryHintHandler.java @@ -0,0 +1,24 @@ +/* + * 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.dialect.hint; + +/** + * Contract defining how query hints get applied. + * + * @author Vlad Mihalcea + */ +public interface QueryHintHandler { + + /** + * Add query hints to the given query. + * + * @param query original query + * @param hints hints to be applied + * @return query with hints + */ + String addQueryHints(String query, String hints); +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/query/spi/NativeSQLQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/engine/query/spi/NativeSQLQueryPlan.java index d9b4f945b0..251c960f20 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/query/spi/NativeSQLQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/query/spi/NativeSQLQueryPlan.java @@ -183,7 +183,12 @@ public class NativeSQLQueryPlan implements Serializable { PreparedStatement ps; try { queryParameters.processFilters( this.customQuery.getSQL(), session ); - final String sql = queryParameters.getFilteredSQL(); + final String sql = session.getJdbcServices().getDialect() + .addSqlHintOrComment( + queryParameters.getFilteredSQL(), + queryParameters, + session.getFactory().getSessionFactoryOptions().isCommentsEnabled() + ); ps = session.getJdbcCoordinator().getStatementPreparer().prepareStatement( sql, false ); diff --git a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/BasicExecutor.java b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/BasicExecutor.java index fd686d8ab8..e000e39ec7 100644 --- a/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/BasicExecutor.java +++ b/hibernate-core/src/main/java/org/hibernate/hql/internal/ast/exec/BasicExecutor.java @@ -56,7 +56,17 @@ public class BasicExecutor implements StatementExecutor { @Override public int execute(QueryParameters parameters, SharedSessionContractImplementor session) throws HibernateException { - return doExecute( parameters, session, sql, parameterSpecifications ); + return doExecute( + parameters, + session, + session.getJdbcServices().getDialect() + .addSqlHintOrComment( + sql, + parameters, + session.getFactory().getSessionFactoryOptions().isCommentsEnabled() + ), + parameterSpecifications + ); } protected int doExecute(QueryParameters parameters, SharedSessionContractImplementor session, String sql, diff --git a/hibernate-core/src/main/java/org/hibernate/loader/Loader.java b/hibernate-core/src/main/java/org/hibernate/loader/Loader.java index 10318a7014..8802af188d 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/Loader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/Loader.java @@ -29,6 +29,7 @@ import org.hibernate.LockOptions; import org.hibernate.QueryException; import org.hibernate.ScrollMode; import org.hibernate.Session; +import org.hibernate.SessionFactory; import org.hibernate.StaleObjectStateException; import org.hibernate.WrongClassException; import org.hibernate.cache.spi.FilterKey; @@ -235,21 +236,20 @@ public abstract class Loader { protected String preprocessSQL( String sql, QueryParameters parameters, - Dialect dialect, + SessionFactoryImplementor sessionFactory, List afterLoadActions) throws HibernateException { + + Dialect dialect = sessionFactory.getServiceRegistry().getService( JdbcServices.class ).getDialect(); + 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() ); - } + sql = dialect.addSqlHintOrComment( + sql, + parameters, + sessionFactory.getSessionFactoryOptions().isCommentsEnabled() + ); - sql = processDistinctKeyword( sql, parameters); - - return getFactory().getSessionFactoryOptions().isCommentsEnabled() - ? prependComment( sql, parameters ) - : sql; + return processDistinctKeyword( sql, parameters ); } protected boolean shouldUseFollowOnLocking( @@ -299,16 +299,6 @@ public abstract class Loader { return lockModeToUse; } - private String prependComment(String sql, QueryParameters parameters) { - String comment = parameters.getComment(); - if ( comment == null ) { - return sql; - } - else { - return "/* " + comment + " */ " + sql; - } - } - /** * Execute an SQL query and attempt to instantiate instances of the class mapped by the given * persister from each row of the ResultSet. If an object is supplied, will attempt to @@ -1920,7 +1910,7 @@ public abstract class Loader { String sql = limitHandler.processSql( queryParameters.getFilteredSQL(), queryParameters.getRowSelection() ); // Adding locks and comments. - sql = preprocessSQL( sql, queryParameters, getFactory().getDialect(), afterLoadActions ); + sql = preprocessSQL( sql, queryParameters, getFactory(), afterLoadActions ); final PreparedStatement st = prepareQueryStatement( sql, queryParameters, limitHandler, scroll, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/AbstractLoadPlanBasedLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/AbstractLoadPlanBasedLoader.java index 1059df8269..bcc8147baa 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/AbstractLoadPlanBasedLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/plan/exec/internal/AbstractLoadPlanBasedLoader.java @@ -180,7 +180,12 @@ public abstract class AbstractLoadPlanBasedLoader { String sql = limitHandler.processSql( queryParameters.getFilteredSQL(), queryParameters.getRowSelection() ); // Adding locks and comments. - sql = preprocessSQL( sql, queryParameters, session.getJdbcServices().getJdbcEnvironment().getDialect(), afterLoadActions ); + sql = session.getJdbcServices().getJdbcEnvironment().getDialect() + .addSqlHintOrComment( + sql, + queryParameters, + session.getFactory().getSessionFactoryOptions().isCommentsEnabled() + ); final PreparedStatement st = prepareQueryStatement( sql, queryParameters, limitHandler, scroll, session ); return new SqlStatementWrapper( st, getResultSet( st, queryParameters.getRowSelection(), limitHandler, queryParameters.hasAutoDiscoverScalarTypes(), session ) ); @@ -198,26 +203,6 @@ public abstract class AbstractLoadPlanBasedLoader { return LimitHelper.useLimit( limitHandler, selection ) ? limitHandler : NoopLimitHandler.INSTANCE; } - private String preprocessSQL( - String sql, - QueryParameters queryParameters, - Dialect dialect, - List afterLoadActions) { - return getFactory().getSettings().isCommentsEnabled() - ? prependComment( sql, queryParameters ) - : sql; - } - - private String prependComment(String sql, QueryParameters parameters) { - final String comment = parameters.getComment(); - if ( comment == null ) { - return sql; - } - else { - return "/* " + comment + " */ " + sql; - } - } - /** * Obtain a PreparedStatement with all parameters pre-bound. * Bind JDBC-style ? parameters, named parameters, and diff --git a/hibernate-core/src/test/java/org/hibernate/jpa/test/query/NamedQueryCommentTest.java b/hibernate-core/src/test/java/org/hibernate/jpa/test/query/NamedQueryCommentTest.java new file mode 100644 index 0000000000..38cd13ec52 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/jpa/test/query/NamedQueryCommentTest.java @@ -0,0 +1,336 @@ +/* + * 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.jpa.test.query; + +import java.sql.Statement; +import java.util.List; +import java.util.Map; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.Index; +import javax.persistence.Query; +import javax.persistence.Table; +import javax.persistence.TypedQuery; + +import org.hibernate.Session; +import org.hibernate.annotations.NamedNativeQuery; +import org.hibernate.annotations.NamedQuery; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.Oracle8iDialect; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; + +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue(jiraKey = "HHH-11640") +public class NamedQueryCommentTest extends BaseEntityManagerFunctionalTestCase { + + private PreparedStatementSpyConnectionProvider connectionProvider; + + @Override + protected Map getConfig() { + Map config = super.getConfig(); + config.put( + org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ); + config.put( + AvailableSettings.USE_SQL_COMMENTS, + Boolean.TRUE.toString() + ); + return config; + } + + @Override + public void buildEntityManagerFactory() throws Exception { + connectionProvider = new PreparedStatementSpyConnectionProvider(); + super.buildEntityManagerFactory(); + } + + @Override + public void releaseResources() { + super.releaseResources(); + connectionProvider.stop(); + } + + private static final String[] GAME_TITLES = { "Halo", "Grand Theft Auto", "NetHack" }; + + @Override + public Class[] getAnnotatedClasses() { + return new Class[] { Game.class }; + } + + @Override + protected void addConfigOptions(Map options) { + options.put( AvailableSettings.USE_SQL_COMMENTS, Boolean.TRUE.toString() ); + } + + @Before + public void setUp() { + doInJPA( this::entityManagerFactory, entityManager -> { + for ( String title : GAME_TITLES ) { + Game game = new Game( title ); + entityManager.persist( game ); + } + } ); + } + + @After + public void tearDown() { + doInJPA( this::entityManagerFactory, entityManager -> { + entityManager.createQuery( "delete from Game" ).executeUpdate(); + } ); + } + + @Test + public void testSelectNamedQueryWithSqlComment() { + doInJPA( this::entityManagerFactory, entityManager -> { + connectionProvider.clear(); + + TypedQuery query = entityManager.createNamedQuery( "SelectNamedQuery", Game.class ); + query.setParameter( "title", GAME_TITLES[0] ); + List list = query.getResultList(); + assertEquals( 1, list.size() ); + + assertEquals( + 1, + connectionProvider.getPreparedStatements().size() + ); + + assertNotNull( + connectionProvider.getPreparedStatement( + "/* INDEX (game idx_game_title) */ select namedquery0_.id as id1_0_, namedquery0_.title as title2_0_ from game namedquery0_ where namedquery0_.title=?" + ) + ); + } ); + } + + @Test + public void testSelectNamedNativeQueryWithSqlComment() { + doInJPA( this::entityManagerFactory, entityManager -> { + connectionProvider.clear(); + + TypedQuery query = entityManager.createNamedQuery( "SelectNamedNativeQuery", Game.class ); + query.setParameter( "title", GAME_TITLES[0] ); + List list = query.getResultList(); + assertEquals( 1, list.size() ); + + assertEquals( + 1, + connectionProvider.getPreparedStatements().size() + ); + + assertNotNull( + connectionProvider.getPreparedStatement( + "/* + INDEX (game idx_game_title) */ select * from Game g where title = ?" + ) + ); + } ); + } + + @Test + public void testUpdateNamedQueryWithSqlComment() { + doInJPA( this::entityManagerFactory, entityManager -> { + connectionProvider.clear(); + + Query query = entityManager.createNamedQuery( "UpdateNamedNativeQuery" ); + query.setParameter( "title", GAME_TITLES[0] ); + query.setParameter( "id", 1L ); + int updateCount = query.executeUpdate(); + assertEquals( 1, updateCount ); + + assertEquals( + 1, + connectionProvider.getPreparedStatements().size() + ); + + assertNotNull( + connectionProvider.getPreparedStatement( + "/* INDEX (game idx_game_title) */ update game set title = ? where id = ?" + ) + ); + } ); + } + + @Test + public void testUpdateNamedNativeQueryWithSqlComment() { + doInJPA( this::entityManagerFactory, entityManager -> { + connectionProvider.clear(); + + Query query = entityManager.createNamedQuery( "UpdateNamedNativeQuery" ); + query.setParameter( "title", GAME_TITLES[0] ); + query.setParameter( "id", 1L ); + int updateCount = query.executeUpdate(); + assertEquals( 1, updateCount ); + + assertEquals( + 1, + connectionProvider.getPreparedStatements().size() + ); + + assertNotNull( + connectionProvider.getPreparedStatement( + "/* INDEX (game idx_game_title) */ update game set title = ? where id = ?" + ) + ); + } ); + } + + @Test + @RequiresDialect(Oracle8iDialect.class) + public void testUpdateNamedNativeQueryWithQueryHintUsingOracle() { + doInJPA( this::entityManagerFactory, entityManager -> { + connectionProvider.clear(); + + Query query = entityManager.createNamedQuery( "UpdateNamedNativeQuery" ); + query.setParameter( "title", GAME_TITLES[0] ); + query.setParameter( "id", 1L ); + query.unwrap( org.hibernate.query.Query.class ).addQueryHint( "INDEX (game idx_game_id)" ); + int updateCount = query.executeUpdate(); + assertEquals( 1, updateCount ); + + assertEquals( + 1, + connectionProvider.getPreparedStatements().size() + ); + + assertNotNull( + connectionProvider.getPreparedStatement( + "update /*+ INDEX (game idx_game_id) */ game set title = ? where id = ?" + ) + ); + } ); + } + + @Test + @RequiresDialect(H2Dialect.class) + public void testUpdateNamedNativeQueryWithQueryHintUsingIndex() { + doInJPA( this::entityManagerFactory, entityManager -> { + connectionProvider.clear(); + + Query query = entityManager.createNamedQuery( "UpdateNamedNativeQuery" ); + query.setParameter( "title", GAME_TITLES[0] ); + query.setParameter( "id", 1L ); + query.unwrap( org.hibernate.query.Query.class ).addQueryHint( "INDEX (game idx_game_id)" ); + int updateCount = query.executeUpdate(); + assertEquals( 1, updateCount ); + + assertEquals( + 1, + connectionProvider.getPreparedStatements().size() + ); + + assertNotNull( + connectionProvider.getPreparedStatement( + "update game set title = ? where id = ?" + ) + ); + } ); + } + + @Test + @RequiresDialect(MySQLDialect.class) + @RequiresDialect(H2Dialect.class) + public void testSelectNamedNativeQueryWithQueryHintUsingIndex() { + doInJPA( this::entityManagerFactory, entityManager -> { + connectionProvider.clear(); + + Query query = entityManager.createNamedQuery( "SelectNamedQuery" ); + query.setParameter( "title", GAME_TITLES[0] ); + query.unwrap( org.hibernate.query.Query.class ).addQueryHint( "idx_game_id" ); + List list = query.getResultList(); + assertEquals( 1, list.size() ); + + assertEquals( + 1, + connectionProvider.getPreparedStatements().size() + ); + + assertNotNull( + connectionProvider.getPreparedStatement( + "select namedquery0_.id as id1_0_, namedquery0_.title as title2_0_ from game namedquery0_ USE INDEX (idx_game_id) where namedquery0_.title=?" + ) + ); + } ); + } + + @Entity(name = "Game") + @Table( + name = "game", + indexes = { + @Index(name = "idx_game_title", columnList = "title"), + @Index(name = "idx_game_id", columnList = "id") + } + ) + @NamedQuery( + name = "SelectNamedQuery", + query = "select g from Game g where title = :title", + comment = "INDEX (game idx_game_title)" + ) + @NamedQuery( + name = "UpdateNamedQuery", + query = "update Game set title = :title where id = :id", + comment = "INDEX (game idx_game_title) " + ) + @NamedNativeQuery( + name = "SelectNamedNativeQuery", + query = "select * from Game g where title = :title", + comment = "+ INDEX (game idx_game_title) ", + resultClass = Game.class + ) + @NamedNativeQuery( + name = "UpdateNamedNativeQuery", + query = "update game set title = :title where id = :id", + comment = "INDEX (game idx_game_title) ", + resultClass = Game.class + ) + public static class Game { + + @Id + @GeneratedValue + private Long id; + + private String title; + + public Game() { + } + + public Game(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/legacy/FooBarTest.java b/hibernate-core/src/test/java/org/hibernate/test/legacy/FooBarTest.java index efa7feb44d..baad73fc87 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/legacy/FooBarTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/legacy/FooBarTest.java @@ -1375,6 +1375,7 @@ public class FooBarTest extends LegacyTestCase { } @Test + @SkipForDialect( value = H2Dialect.class, comment = "Feature not supported: MVCC=TRUE && FOR UPDATE && JOIN") public void testQueryLockMode() throws Exception { Session s = openSession(); Transaction tx = s.beginTransaction(); @@ -4029,6 +4030,7 @@ public class FooBarTest extends LegacyTestCase { } @SkipForDialect(value = AbstractHANADialect.class, comment = "HANA currently requires specifying table name by 'FOR UPDATE of t1.c1' if there are more than one tables/views/subqueries in the FROM clause") + @SkipForDialect( value = H2Dialect.class, comment = "Feature not supported: MVCC=TRUE && FOR UPDATE && JOIN") @Test public void testNewSessionLifecycle() throws Exception { Session s = openSession(); @@ -4309,6 +4311,7 @@ public class FooBarTest extends LegacyTestCase { } @SkipForDialect(value = AbstractHANADialect.class, comment = "HANA currently requires specifying table name by 'FOR UPDATE of t1.c1' if there are more than one tables/views/subqueries in the FROM clause") + @SkipForDialect( value = H2Dialect.class, comment = "Feature not supported: MVCC=TRUE && FOR UPDATE && JOIN") @Test public void testRefresh() throws Exception { final Session s = openSession(); diff --git a/hibernate-core/src/test/java/org/hibernate/test/legacy/MultiTableTest.java b/hibernate-core/src/test/java/org/hibernate/test/legacy/MultiTableTest.java index 38918f9bc2..27051f04d1 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/legacy/MultiTableTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/legacy/MultiTableTest.java @@ -30,6 +30,7 @@ import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.criterion.Restrictions; import org.hibernate.dialect.AbstractHANADialect; +import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.MySQLDialect; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.jdbc.Work; @@ -369,7 +370,8 @@ public class MultiTableTest extends LegacyTestCase { // HANA currently requires specifying table name by 'FOR UPDATE of t1.c1' // if there are more than one tables/views/subqueries in the FROM clause - if ( !( getDialect() instanceof AbstractHANADialect ) ) { + // H2 - Feature not supported: MVCC=TRUE && FOR UPDATE && JOIN + if ( !( getDialect() instanceof AbstractHANADialect || getDialect() instanceof H2Dialect ) ) { s = openSession(); t = s.beginTransaction(); multi = (Multi) s.load( Top.class, mid, LockMode.UPGRADE ); @@ -491,7 +493,8 @@ public class MultiTableTest extends LegacyTestCase { // HANA currently requires specifying table name by 'FOR UPDATE of t1.c1' // if there are more than one tables/views/subqueries in the FROM clause - if ( !( getDialect() instanceof AbstractHANADialect ) ) { + // H2 - Feature not supported: MVCC=TRUE && FOR UPDATE && JOIN + if ( !( getDialect() instanceof AbstractHANADialect || getDialect() instanceof H2Dialect ) ) { s = openSession(); t = s.beginTransaction(); multi = (Multi) s.load( Top.class, multiId, LockMode.UPGRADE ); diff --git a/hibernate-core/src/test/java/org/hibernate/test/legacy/ParentChildTest.java b/hibernate-core/src/test/java/org/hibernate/test/legacy/ParentChildTest.java index d3ad05f50b..395ea2e4de 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/legacy/ParentChildTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/legacy/ParentChildTest.java @@ -32,6 +32,7 @@ import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.criterion.Restrictions; import org.hibernate.dialect.AbstractHANADialect; +import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.IngresDialect; import org.hibernate.dialect.MySQLDialect; @@ -225,6 +226,7 @@ public class ParentChildTest extends LegacyTestCase { } @Test + @SkipForDialect( value = H2Dialect.class, comment = "Feature not supported: MVCC=TRUE && FOR UPDATE && JOIN") public void testComplexCriteria() throws Exception { Session s = openSession(); Transaction t = s.beginTransaction(); diff --git a/libraries.gradle b/libraries.gradle index 533f6a1e9f..54a4b3de60 100644 --- a/libraries.gradle +++ b/libraries.gradle @@ -10,8 +10,7 @@ ext { junitVersion = '4.12' -// h2Version = '1.2.145' - h2Version = '1.3.176' + h2Version = '1.4.196' bytemanVersion = '4.0.0-BETA5' infinispanVersion = '8.2.5.Final' jnpVersion = '5.0.6.CR1'