HHH-12469 - Add support for IN clause parameter padding to better reuse cached statements
This commit is contained in:
parent
676aebdf51
commit
43d15578dc
|
@ -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;
|
||||
|
|
|
@ -417,4 +417,9 @@ public class AbstractDelegatingSessionFactoryOptions implements SessionFactoryOp
|
|||
public ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
|
||||
return delegate.getImmutableEntityUpdateQueryHandlingMode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean inClauseParameterPaddingEnabled() {
|
||||
return delegate.inClauseParameterPaddingEnabled();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -280,4 +280,8 @@ public interface SessionFactoryOptions {
|
|||
default ImmutableEntityUpdateQueryHandlingMode getImmutableEntityUpdateQueryHandlingMode() {
|
||||
return ImmutableEntityUpdateQueryHandlingMode.WARNING;
|
||||
}
|
||||
|
||||
default boolean inClauseParameterPaddingEnabled() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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 ) );
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue