HHH-12469 - Add support for IN clause parameter padding to better reuse cached statements

This commit is contained in:
Vlad Mihalcea 2018-04-13 11:31:31 +03:00
parent 676aebdf51
commit 43d15578dc
9 changed files with 401 additions and 8 deletions

View File

@ -89,6 +89,7 @@ import static org.hibernate.cfg.AvailableSettings.GENERATE_STATISTICS;
import static org.hibernate.cfg.AvailableSettings.HQL_BULK_ID_STRATEGY;
import static org.hibernate.cfg.AvailableSettings.IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE;
import static org.hibernate.cfg.AvailableSettings.INTERCEPTOR;
import static org.hibernate.cfg.AvailableSettings.IN_CLAUSE_PARAMETER_PADDING;
import static org.hibernate.cfg.AvailableSettings.JDBC_TIME_ZONE;
import static org.hibernate.cfg.AvailableSettings.JDBC_TYLE_PARAMS_ZERO_BASE;
import static org.hibernate.cfg.AvailableSettings.JTA_TRACK_BY_THREAD;
@ -234,6 +235,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
private JpaCompliance jpaCompliance;
private boolean failOnPaginationOverCollectionFetchEnabled;
private boolean inClauseParameterPaddingEnabled;
@SuppressWarnings({"WeakerAccess", "deprecation"})
public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, BootstrapContext context) {
@ -487,6 +489,12 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
this.immutableEntityUpdateQueryHandlingMode = ImmutableEntityUpdateQueryHandlingMode.interpret(
configurationSettings.get( IMMUTABLE_ENTITY_UPDATE_QUERY_HANDLING_MODE )
);
this.inClauseParameterPaddingEnabled = ConfigurationHelper.getBoolean(
IN_CLAUSE_PARAMETER_PADDING,
configurationSettings,
false
);
}
@SuppressWarnings("deprecation")
@ -985,6 +993,11 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
return this.failOnPaginationOverCollectionFetchEnabled;
}
@Override
public boolean inClauseParameterPaddingEnabled() {
return this.inClauseParameterPaddingEnabled;
}
@Override
public JpaCompliance getJpaCompliance() {
return jpaCompliance;

View File

@ -417,4 +417,9 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp
public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
return delegate.getImmutableEntityUpdateQueryHandlingMode();
}
@Override
public boolean inClauseParameterPaddingEnabled() {
return delegate.inClauseParameterPaddingEnabled();
}
}

View File

@ -280,4 +280,8 @@ public interface SessionFactoryOptions {
default ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
return ImmutableEntityUpdateQueryHandlingMode.WARNING;
}
default boolean inClauseParameterPaddingEnabled() {
return false;
}
}

View File

@ -1904,4 +1904,22 @@ public interface AvailableSettings extends org.hibernate.jpa.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.
* </p>
* 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.
* </p>
* 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.
* </p>
* If you want to activate this feature, you need to set this property to {@code true}.
* </p>
* The default value is {@code false}.
*
* @since 5.2.17
*/
String IN_CLAUSE_PARAMETER_PADDING = "hibernate.query.in_clause_parameter_padding";
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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);
}
}

View File

@ -10,12 +10,11 @@ import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.persistence.Parameter;
import org.hibernate.HibernateException;
@ -31,6 +30,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.engine.spi.TypedValue;
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.internal.util.collections.ArrayHelper;
import org.hibernate.internal.util.collections.CollectionHelper;
@ -528,8 +528,23 @@ public class QueryParameterBindingsImpl implements QueryParameterBindings {
final QueryParameter sourceParam = 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 String sourceToken;
@ -566,8 +581,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();
}
if ( i > 0 ) {
expansionList.append( ", " );
}
@ -613,8 +635,6 @@ public class QueryParameterBindingsImpl implements QueryParameterBindings {
final QueryParameterBinding syntheticBinding = makeBinding( entry.getValue().getBindType() );
syntheticBinding.setBindValue( bindValue );
parameterBindingMap.put( syntheticParam, syntheticBinding );
i++;
}
queryString = StringHelper.replace(

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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 ) );
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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;
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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;
}
}
}