diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryBuilderImpl.java index 7a05b22437..cdda6708f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryBuilderImpl.java @@ -583,6 +583,8 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement private boolean jpaProxyComplianceEnabled; private ImmutableEntityUpdateQueryHandlingMode immutableEntityUpdateQueryHandlingMode; + private boolean inClauseParameterPaddingEnabled; + public SessionFactoryOptionsStateStandardImpl(StandardServiceRegistry serviceRegistry) { this.serviceRegistry = serviceRegistry; @@ -829,6 +831,12 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement this.immutableEntityUpdateQueryHandlingMode = ImmutableEntityUpdateQueryHandlingMode.interpret( configurationSettings.get( IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE ) ); + + this.inClauseParameterPaddingEnabled = ConfigurationHelper.getBoolean( + IN_CLAUSE_PARAMETER_PADDING, + configurationSettings, + false + ); } private static Interceptor determineInterceptor(Map configurationSettings, StrategySelector strategySelector) { @@ -1297,6 +1305,11 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() { return immutableEntityUpdateQueryHandlingMode; } + + @Override + public boolean inClauseParameterPaddingEnabled() { + return this.inClauseParameterPaddingEnabled; + } } private static Supplier interceptorSupplier(Class clazz) { @@ -1661,4 +1674,9 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() { return options.getImmutableEntityUpdateQueryHandlingMode(); } + + @Override + public boolean inClauseParameterPaddingEnabled() { + return options.inClauseParameterPaddingEnabled(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsImpl.java index d07d6f9e3c..64ff7860ef 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsImpl.java @@ -137,6 +137,7 @@ public class SessionFactoryOptionsImpl implements SessionFactoryOptions { private final boolean failOnPaginationOverCollectionFetchEnabled; private final boolean jpaProxyComplianceEnabled; private ImmutableEntityUpdateQueryHandlingMode immutableEntityUpdateQueryHandlingMode; + private boolean inClauseParameterPaddingEnabled; public SessionFactoryOptionsImpl(SessionFactoryOptionsState state) { this.serviceRegistry = state.getServiceRegistry(); @@ -225,6 +226,8 @@ public class SessionFactoryOptionsImpl implements SessionFactoryOptions { this.jpaProxyComplianceEnabled = state.isJpaProxyComplianceEnabled(); this.immutableEntityUpdateQueryHandlingMode = state.getImmutableEntityUpdateQueryHandlingMode(); + + this.inClauseParameterPaddingEnabled = state.inClauseParameterPaddingEnabled(); } @Override @@ -585,4 +588,9 @@ public class SessionFactoryOptionsImpl implements SessionFactoryOptions { public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() { return immutableEntityUpdateQueryHandlingMode; } + + @Override + public boolean inClauseParameterPaddingEnabled() { + return inClauseParameterPaddingEnabled; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsState.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsState.java index f6836c9ccf..89b7ec353b 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsState.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsState.java @@ -216,4 +216,8 @@ public interface SessionFactoryOptionsState { default ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() { return ImmutableEntityUpdateQueryHandlingMode.WARNING; } + + default boolean inClauseParameterPaddingEnabled() { + return false; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java index f196ec00dc..ca30ec7498 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java @@ -411,4 +411,9 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() { return delegate.getImmutableEntityUpdateQueryHandlingMode(); } + + @Override + public boolean inClauseParameterPaddingEnabled() { + return delegate.inClauseParameterPaddingEnabled(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java index ae18de2ca5..9805aab36b 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java @@ -261,4 +261,8 @@ public interface SessionFactoryOptions { default ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() { return ImmutableEntityUpdateQueryHandlingMode.WARNING; } + + default boolean inClauseParameterPaddingEnabled() { + return false; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java index 6e758b462d..dd4a38b8e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java @@ -1779,4 +1779,22 @@ public interface AvailableSettings { * @see org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode */ String IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE = "hibernate.query.immutable_entity_update_query_handling_mode"; + + /** + * By default, the IN clause expands to include all bind parameter values. + *

+ * However, for database systems supporting execution plan caching, + * there's a better chance of hitting the cache if the number of possible IN clause parameters lowers. + *

+ * For this reason, we can expand the bind parameters to power-of-two: 4, 8, 16, 32, 64. + * This way, an IN clause with 5, 6, or 7 bind parameters will use the 8 IN clause, + * therefore reusing its execution plan. + *

+ * If you want to activate this feature, you need to set this property to {@code true}. + *

+ * The default value is {@code false}. + * + * @since 5.2.17 + */ + String IN_CLAUSE_PARAMETER_PADDING = "hibernate.query.in_clause_parameter_padding"; } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/MathHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/MathHelper.java new file mode 100644 index 0000000000..64ef20feed --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/MathHelper.java @@ -0,0 +1,26 @@ +/* + * 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.internal.util; + +/** + * @author Vlad Mihalcea + */ +public final class MathHelper { + + private MathHelper() { /* static methods only - hide constructor */ + } + + /** + * Returns the smallest power of two number that is greater than or equal to {@code value}. + * + * @param value reference number + * @return smallest power of two number + */ + public static int ceilingPowerOfTwo(int value) { + return 1 << -Integer.numberOfLeadingZeros(value - 1); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java index 29983b6237..a17b5f043f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingsImpl.java @@ -28,6 +28,7 @@ import org.hibernate.engine.spi.TypedValue; import org.hibernate.hql.internal.classic.ParserHelper; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.internal.util.MathHelper; import org.hibernate.internal.util.StringHelper; import org.hibernate.query.ParameterMetadata; import org.hibernate.query.QueryParameter; @@ -548,8 +549,23 @@ public class QueryParameterBindingsImpl implements QueryParameterBindings { final NamedParameterDescriptor sourceParam = (NamedParameterDescriptor) entry.getKey(); final Collection bindValues = entry.getValue().getBindValues(); - if ( inExprLimit > 0 && bindValues.size() > inExprLimit ) { - log.tooManyInExpressions( dialect.getClass().getName(), inExprLimit, sourceParam.getName(), bindValues.size() ); + int bindValueCount = bindValues.size(); + int bindValueMaxCount = bindValueCount; + + boolean inClauseParameterPaddingEnabled = + session.getFactory().getSessionFactoryOptions().inClauseParameterPaddingEnabled() && + bindValueCount > 2; + + if ( inClauseParameterPaddingEnabled ) { + int bindValuePaddingCount = MathHelper.ceilingPowerOfTwo( bindValueCount ); + + if ( bindValueCount < bindValuePaddingCount && (inExprLimit == 0 || bindValuePaddingCount < inExprLimit) ) { + bindValueMaxCount = bindValuePaddingCount; + } + } + + if ( inExprLimit > 0 && bindValueCount > inExprLimit ) { + log.tooManyInExpressions( dialect.getClass().getName(), inExprLimit, sourceParam.getName(), bindValueCount ); } final boolean isJpaPositionalParam = sourceParam.isJpaPositionalParameter(); @@ -581,8 +597,15 @@ public class QueryParameterBindingsImpl implements QueryParameterBindings { StringBuilder expansionList = new StringBuilder(); - int i = 0; - for ( Object bindValue : entry.getValue().getBindValues() ) { + Iterator bindValueIterator = entry.getValue().getBindValues().iterator(); + Object bindValue = null; + + for ( int i = 0; i < bindValueMaxCount; i++ ) { + + if ( i < bindValueCount ) { + bindValue = bindValueIterator.next(); + } + // for each value in the bound list-of-values we: // 1) create a synthetic named parameter // 2) expand the queryString to include each synthetic named param in place of the original @@ -601,7 +624,6 @@ public class QueryParameterBindingsImpl implements QueryParameterBindings { final QueryParameterBinding syntheticBinding = makeBinding( entry.getValue().getBindType() ); syntheticBinding.setBindValue( bindValue ); parameterBindingMap.put( syntheticParam, syntheticBinding ); - i++; } queryString = StringHelper.replace( diff --git a/hibernate-core/src/test/java/org/hibernate/internal/util/MathHelperTest.java b/hibernate-core/src/test/java/org/hibernate/internal/util/MathHelperTest.java new file mode 100644 index 0000000000..6f44048b22 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/internal/util/MathHelperTest.java @@ -0,0 +1,38 @@ +/* + * 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.internal.util; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * @author Vlad Mihalcea + */ +public class MathHelperTest { + + @Test + public void ceilingPowerOfTwo() { + assertEquals( 1, MathHelper.ceilingPowerOfTwo( 1 ) ); + assertEquals( 2, MathHelper.ceilingPowerOfTwo( 2 ) ); + assertEquals( 4, MathHelper.ceilingPowerOfTwo( 3 ) ); + assertEquals( 4, MathHelper.ceilingPowerOfTwo( 4 ) ); + assertEquals( 8, MathHelper.ceilingPowerOfTwo( 5 ) ); + assertEquals( 8, MathHelper.ceilingPowerOfTwo( 6 ) ); + assertEquals( 8, MathHelper.ceilingPowerOfTwo( 7 ) ); + assertEquals( 8, MathHelper.ceilingPowerOfTwo( 8 ) ); + assertEquals( 16, MathHelper.ceilingPowerOfTwo( 9 ) ); + assertEquals( 16, MathHelper.ceilingPowerOfTwo( 10 ) ); + assertEquals( 16, MathHelper.ceilingPowerOfTwo( 11 ) ); + assertEquals( 16, MathHelper.ceilingPowerOfTwo( 12 ) ); + assertEquals( 16, MathHelper.ceilingPowerOfTwo( 13 ) ); + assertEquals( 16, MathHelper.ceilingPowerOfTwo( 16 ) ); + assertEquals( 16, MathHelper.ceilingPowerOfTwo( 14 ) ); + assertEquals( 16, MathHelper.ceilingPowerOfTwo( 15 ) ); + } + +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/query/InClauseParameterPaddingTest.java b/hibernate-core/src/test/java/org/hibernate/query/InClauseParameterPaddingTest.java new file mode 100644 index 0000000000..f2bc0a869f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/query/InClauseParameterPaddingTest.java @@ -0,0 +1,129 @@ +/* + * 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.query; + +import java.util.Arrays; +import java.util.Map; +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.test.util.jdbc.PreparedStatementSpyConnectionProvider; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue( jiraKey = "HHH-12469" ) +public class InClauseParameterPaddingTest extends BaseEntityManagerFunctionalTestCase { + + private PreparedStatementSpyConnectionProvider connectionProvider; + + @Override + public void buildEntityManagerFactory() { + connectionProvider = new PreparedStatementSpyConnectionProvider(); + super.buildEntityManagerFactory(); + } + + @Override + public void releaseResources() { + super.releaseResources(); + connectionProvider.stop(); + } + + @Override + public Class[] getAnnotatedClasses() { + return new Class[] { + Person.class + }; + } + + @Override + protected void addConfigOptions(Map options) { + options.put( AvailableSettings.IN_CLAUSE_PARAMETER_PADDING, Boolean.TRUE.toString() ); + options.put( + org.hibernate.cfg.AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ); + } + + @Override + protected void afterEntityManagerFactoryBuilt() { + doInJPA( this::entityManagerFactory, entityManager -> { + for ( int i = 1; i < 10; i++ ) { + Person person = new Person(); + person.setId( i ); + person.setName( String.format( "Person nr %d", i ) ); + + entityManager.persist( person ); + } + } ); + } + + @Test + public void testInClauseParameterPadding() { + validateInClauseParameterPadding( "in (?)", 1 ); + validateInClauseParameterPadding( "in (? , ?)", 1, 2 ); + validateInClauseParameterPadding( "in (? , ? , ? , ?)", 1, 2, 3 ); + validateInClauseParameterPadding( "in (? , ? , ? , ?)", 1, 2, 3, 4 ); + validateInClauseParameterPadding( "in (? , ? , ? , ? , ? , ? , ? , ?)", 1, 2, 3, 4, 5 ); + validateInClauseParameterPadding( "in (? , ? , ? , ? , ? , ? , ? , ?)", 1, 2, 3, 4, 5, 6 ); + validateInClauseParameterPadding( "in (? , ? , ? , ? , ? , ? , ? , ?)", 1, 2, 3, 4, 5, 6, 7 ); + validateInClauseParameterPadding( "in (? , ? , ? , ? , ? , ? , ? , ?)", 1, 2, 3, 4, 5, 6, 7, 8 ); + validateInClauseParameterPadding( "in (? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ?)", 1, 2, 3, 4, 5, 6, 7, 8, 9 ); + validateInClauseParameterPadding( "in (? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ? , ?)", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ); + } + + private void validateInClauseParameterPadding(String expectedInClause, Integer... ids) { + connectionProvider.clear(); + + doInJPA( this::entityManagerFactory, entityManager -> { + return entityManager.createQuery( + "select p " + + "from Person p " + + "where p.id in :ids" ) + .setParameter( "ids", Arrays.asList(ids) ) + .getResultList(); + } ); + + assertTrue(connectionProvider.getPreparedSQLStatements().get( 0 ).endsWith( expectedInClause )); + } + + @Entity(name = "Person") + public static class Person { + + @Id + private Integer id; + + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/query/MaxInExpressionParameterPaddingTest.java b/hibernate-core/src/test/java/org/hibernate/query/MaxInExpressionParameterPaddingTest.java new file mode 100644 index 0000000000..b60e602860 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/query/MaxInExpressionParameterPaddingTest.java @@ -0,0 +1,140 @@ +/* + * 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.query; + +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.H2Dialect; +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.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertTrue; + +/** + * @author Vlad Mihalcea + */ +@TestForIssue( jiraKey = "HHH-12469" ) +@RequiresDialect(H2Dialect.class) +public class MaxInExpressionParameterPaddingTest extends BaseEntityManagerFunctionalTestCase { + + private PreparedStatementSpyConnectionProvider connectionProvider; + + public static final int MAX_COUNT = 15; + + @Override + public void buildEntityManagerFactory() { + connectionProvider = new PreparedStatementSpyConnectionProvider(); + super.buildEntityManagerFactory(); + } + + @Override + public void releaseResources() { + super.releaseResources(); + connectionProvider.stop(); + } + + @Override + public Class[] getAnnotatedClasses() { + return new Class[] { + Person.class + }; + } + + @Override + protected void addConfigOptions(Map options) { + options.put( AvailableSettings.IN_CLAUSE_PARAMETER_PADDING, Boolean.TRUE.toString() ); + options.put( + AvailableSettings.CONNECTION_PROVIDER, + connectionProvider + ); + } + + @Override + protected Dialect getDialect() { + return new MaxCountInExpressionH2Dialect(); + } + + @Override + protected void afterEntityManagerFactoryBuilt() { + doInJPA( this::entityManagerFactory, entityManager -> { + for ( int i = 0; i < MAX_COUNT; i++ ) { + Person person = new Person(); + person.setId( i ); + person.setName( String.format( "Person nr %d", i ) ); + + entityManager.persist( person ); + } + } ); + } + + @Test + public void testInClauseParameterPadding() { + connectionProvider.clear(); + + doInJPA( this::entityManagerFactory, entityManager -> { + return entityManager.createQuery( + "select p " + + "from Person p " + + "where p.id in :ids" ) + .setParameter( "ids", IntStream.range( 0, MAX_COUNT ).boxed().collect(Collectors.toList()) ) + .getResultList(); + } ); + + StringBuilder expectedInClause = new StringBuilder(); + expectedInClause.append( "in (?" ); + for ( int i = 1; i < MAX_COUNT; i++ ) { + expectedInClause.append( " , ?" ); + } + expectedInClause.append( ")" ); + + assertTrue(connectionProvider.getPreparedSQLStatements().get( 0 ).endsWith( expectedInClause.toString() )); + } + + @Entity(name = "Person") + public static class Person { + + @Id + private Integer id; + + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + public static class MaxCountInExpressionH2Dialect extends H2Dialect { + @Override + public int getInExpressionCountLimit() { + return MAX_COUNT; + } + } + +}