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 6c42a17210
commit 74d0fd0dbe
11 changed files with 417 additions and 5 deletions

View File

@ -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<? extends Interceptor> interceptorSupplier(Class<? extends Interceptor> clazz) {
@ -1661,4 +1674,9 @@ public class SessionFactoryBuilderImpl implements SessionFactoryBuilderImplement
public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
return options.getImmutableEntityUpdateQueryHandlingMode();
}
@Override
public boolean inClauseParameterPaddingEnabled() {
return options.inClauseParameterPaddingEnabled();
}
}

View File

@ -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;
}
}

View File

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

View File

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

View File

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

View File

@ -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.
* </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

@ -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(

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;
}
}
}