Skip to content

Commit 06b0faa

Browse files
committed
HHH-7746 - Investigate alternative batch loading algorithms
1 parent d41545e commit 06b0faa

30 files changed

+1734
-236
lines changed

Diff for: hibernate-core/src/main/java/org/hibernate/cfg/AvailableSettings.java

+7
Original file line numberDiff line numberDiff line change
@@ -579,4 +579,11 @@ public interface AvailableSettings {
579579
public static final String ENABLE_LAZY_LOAD_NO_TRANS = "hibernate.enable_lazy_load_no_trans";
580580

581581
public static final String HQL_BULK_ID_STRATEGY = "hibernate.hql.bulk_id_strategy";
582+
583+
/**
584+
* Names the {@link org.hibernate.loader.BatchFetchStyle} to use. Can specify either the
585+
* {@link org.hibernate.loader.BatchFetchStyle} name (insensitively), or a
586+
* {@link org.hibernate.loader.BatchFetchStyle} instance.
587+
*/
588+
public static final String BATCH_FETCH_STYLE = "hibernate.batch_fetch_style";
582589
}

Diff for: hibernate-core/src/main/java/org/hibernate/cfg/Settings.java

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.hibernate.hql.spi.MultiTableBulkIdStrategy;
3434
import org.hibernate.hql.spi.QueryTranslatorFactory;
3535
import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform;
36+
import org.hibernate.loader.BatchFetchStyle;
3637
import org.hibernate.tuple.entity.EntityTuplizerFactory;
3738

3839
/**
@@ -91,6 +92,7 @@ public final class Settings {
9192
private JtaPlatform jtaPlatform;
9293

9394
private MultiTableBulkIdStrategy multiTableBulkIdStrategy;
95+
private BatchFetchStyle batchFetchStyle;
9496

9597

9698
/**
@@ -480,4 +482,12 @@ public MultiTableBulkIdStrategy getMultiTableBulkIdStrategy() {
480482
void setMultiTableBulkIdStrategy(MultiTableBulkIdStrategy multiTableBulkIdStrategy) {
481483
this.multiTableBulkIdStrategy = multiTableBulkIdStrategy;
482484
}
485+
486+
public BatchFetchStyle getBatchFetchStyle() {
487+
return batchFetchStyle;
488+
}
489+
490+
void setBatchFetchStyle(BatchFetchStyle batchFetchStyle) {
491+
this.batchFetchStyle = batchFetchStyle;
492+
}
483493
}

Diff for: hibernate-core/src/main/java/org/hibernate/cfg/SettingsFactory.java

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.hibernate.internal.CoreMessageLogger;
5050
import org.hibernate.internal.util.StringHelper;
5151
import org.hibernate.internal.util.config.ConfigurationHelper;
52+
import org.hibernate.loader.BatchFetchStyle;
5253
import org.hibernate.service.ServiceRegistry;
5354
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
5455
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
@@ -199,6 +200,11 @@ public Settings buildSettings(Properties props, ServiceRegistry serviceRegistry)
199200
}
200201
settings.setConnectionReleaseMode( releaseMode );
201202

203+
final BatchFetchStyle batchFetchStyle = BatchFetchStyle.interpret( properties.get( AvailableSettings.BATCH_FETCH_STYLE ) );
204+
LOG.debugf( "Using BatchFetchStyle : " + batchFetchStyle.name() );
205+
settings.setBatchFetchStyle( batchFetchStyle );
206+
207+
202208
//SQL Generation settings:
203209

204210
String defaultSchema = properties.getProperty( AvailableSettings.DEFAULT_SCHEMA );

Diff for: hibernate-core/src/main/java/org/hibernate/internal/util/StringHelper.java

+86
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
*/
2525
package org.hibernate.internal.util;
2626

27+
import java.io.Serializable;
2728
import java.util.ArrayList;
2829
import java.util.Arrays;
2930
import java.util.Iterator;
@@ -66,6 +67,17 @@ public static String join(String seperator, String[] strings) {
6667
return buf.toString();
6768
}
6869

70+
public static String joinWithQualifier(String[] values, String qualifier, String deliminator) {
71+
int length = values.length;
72+
if ( length == 0 ) return "";
73+
StringBuilder buf = new StringBuilder( length * values[0].length() )
74+
.append( qualify( qualifier, values[0] ) );
75+
for ( int i = 1; i < length; i++ ) {
76+
buf.append( deliminator ).append( qualify( qualifier, values[i] ) );
77+
}
78+
return buf.toString();
79+
}
80+
6981
public static String join(String seperator, Iterator objects) {
7082
StringBuilder buf = new StringBuilder();
7183
if ( objects.hasNext() ) buf.append( objects.next() );
@@ -89,6 +101,15 @@ public static String repeat(String string, int times) {
89101
return buf.toString();
90102
}
91103

104+
public static String repeat(String string, int times, String deliminator) {
105+
StringBuilder buf = new StringBuilder( ( string.length() * times ) + ( deliminator.length() * (times-1) ) )
106+
.append( string );
107+
for ( int i = 1; i < times; i++ ) {
108+
buf.append( deliminator ).append( string );
109+
}
110+
return buf.toString();
111+
}
112+
92113
public static String repeat(char character, int times) {
93114
char[] buffer = new char[times];
94115
Arrays.fill( buffer, character );
@@ -661,4 +682,69 @@ public static String[] unquote(String[] names, Dialect dialect) {
661682
}
662683
return unquoted;
663684
}
685+
686+
687+
public static final String BATCH_ID_PLACEHOLDER = "$$BATCH_ID_PLACEHOLDER$$";
688+
689+
public static StringBuilder buildBatchFetchRestrictionFragment(
690+
String alias,
691+
String[] columnNames,
692+
Dialect dialect) {
693+
// the general idea here is to just insert a placeholder that we can easily find later...
694+
if ( columnNames.length == 1 ) {
695+
// non-composite key
696+
return new StringBuilder( StringHelper.qualify( alias, columnNames[0] ) )
697+
.append( " in (" ).append( BATCH_ID_PLACEHOLDER ).append( ")" );
698+
}
699+
else {
700+
// composite key - the form to use here depends on what the dialect supports.
701+
if ( dialect.supportsRowValueConstructorSyntaxInInList() ) {
702+
// use : (col1, col2) in ( (?,?), (?,?), ... )
703+
StringBuilder builder = new StringBuilder();
704+
builder.append( "(" );
705+
boolean firstPass = true;
706+
String deliminator = "";
707+
for ( String columnName : columnNames ) {
708+
builder.append( deliminator ).append( StringHelper.qualify( alias, columnName ) );
709+
if ( firstPass ) {
710+
firstPass = false;
711+
deliminator = ",";
712+
}
713+
}
714+
builder.append( ") in (" );
715+
builder.append( BATCH_ID_PLACEHOLDER );
716+
builder.append( ")" );
717+
return builder;
718+
}
719+
else {
720+
// use : ( (col1 = ? and col2 = ?) or (col1 = ? and col2 = ?) or ... )
721+
// unfortunately most of this building needs to be held off until we know
722+
// the exact number of ids :(
723+
return new StringBuilder( "(" ).append( BATCH_ID_PLACEHOLDER ).append( ")" );
724+
}
725+
}
726+
}
727+
728+
public static String expandBatchIdPlaceholder(
729+
String sql,
730+
Serializable[] ids,
731+
String alias,
732+
String[] keyColumnNames,
733+
Dialect dialect) {
734+
if ( keyColumnNames.length == 1 ) {
735+
// non-composite
736+
return StringHelper.replace( sql, BATCH_ID_PLACEHOLDER, repeat( "?", ids.length, "," ) );
737+
}
738+
else {
739+
// composite
740+
if ( dialect.supportsRowValueConstructorSyntaxInInList() ) {
741+
final String tuple = "(" + StringHelper.repeat( "?", keyColumnNames.length, "," );
742+
return StringHelper.replace( sql, BATCH_ID_PLACEHOLDER, repeat( tuple, ids.length, "," ) );
743+
}
744+
else {
745+
final String keyCheck = joinWithQualifier( keyColumnNames, alias, " and " );
746+
return replace( sql, BATCH_ID_PLACEHOLDER, repeat( keyCheck, ids.length, " or " ) );
747+
}
748+
}
749+
}
664750
}

Diff for: hibernate-core/src/main/java/org/hibernate/internal/util/collections/ArrayHelper.java

+37-2
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
*/
2525
package org.hibernate.internal.util.collections;
2626

27+
import java.io.Serializable;
2728
import java.lang.reflect.Array;
2829
import java.util.ArrayList;
2930
import java.util.Arrays;
3031
import java.util.Collection;
3132
import java.util.Iterator;
3233
import java.util.List;
3334

35+
import org.hibernate.HibernateException;
3436
import org.hibernate.LockMode;
3537
import org.hibernate.LockOptions;
3638
import org.hibernate.type.Type;
@@ -372,10 +374,43 @@ public static boolean isEquals(byte[] b1, byte[] b2) {
372374
}
373375
return true;
374376
}
375-
}
376-
377377

378+
public static Serializable[] extractNonNull(Serializable[] array) {
379+
final int nonNullCount = countNonNull( array );
380+
final Serializable[] result = new Serializable[nonNullCount];
381+
int i = 0;
382+
for ( Serializable element : array ) {
383+
if ( element != null ) {
384+
result[i++] = element;
385+
}
386+
}
387+
if ( i != nonNullCount ) {
388+
throw new HibernateException( "Number of non-null elements varied between iterations" );
389+
}
390+
return result;
391+
}
378392

393+
public static int countNonNull(Serializable[] array) {
394+
int i = 0;
395+
for ( Serializable element : array ) {
396+
if ( element != null ) {
397+
i++;
398+
}
399+
}
400+
return i;
401+
}
379402

403+
public static void main(String... args) {
404+
int[] batchSizes = ArrayHelper.getBatchSizes( 32 );
380405

406+
System.out.println( "Forward ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
407+
for ( int i = 0; i < batchSizes.length; i++ ) {
408+
System.out.println( "[" + i + "] -> " + batchSizes[i] );
409+
}
381410

411+
System.out.println( "Backward ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" );
412+
for ( int i = batchSizes.length-1; i >= 0; i-- ) {
413+
System.out.println( "[" + i + "] -> " + batchSizes[i] );
414+
}
415+
}
416+
}

Diff for: hibernate-core/src/main/java/org/hibernate/loader/AbstractEntityJoinWalker.java

+3-6
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,7 @@ protected final boolean isJoinFetchEnabledByProfile(OuterJoinLoadable persister,
187187
public abstract String getComment();
188188

189189
@Override
190-
protected boolean isDuplicateAssociation(
191-
final String foreignKeyTable,
192-
final String[] foreignKeyColumns
193-
) {
190+
protected boolean isDuplicateAssociation(final String foreignKeyTable, final String[] foreignKeyColumns) {
194191
//disable a join back to this same association
195192
final boolean isSameJoin =
196193
persister.getTableName().equals( foreignKeyTable ) &&
@@ -201,11 +198,11 @@ protected boolean isDuplicateAssociation(
201198

202199

203200

204-
protected final Loadable getPersister() {
201+
public final Loadable getPersister() {
205202
return persister;
206203
}
207204

208-
protected final String getAlias() {
205+
public final String getAlias() {
209206
return alias;
210207
}
211208

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* Copyright (c) 2012, Red Hat Inc. or third-party contributors as
5+
* indicated by the @author tags or express copyright attribution
6+
* statements applied by the authors. All third-party contributions are
7+
* distributed under license by Red Hat Inc.
8+
*
9+
* This copyrighted material is made available to anyone wishing to use, modify,
10+
* copy, or redistribute it subject to the terms and conditions of the GNU
11+
* Lesser General Public License, as published by the Free Software Foundation.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15+
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
16+
* for more details.
17+
*
18+
* You should have received a copy of the GNU Lesser General Public License
19+
* along with this distribution; if not, write to:
20+
* Free Software Foundation, Inc.
21+
* 51 Franklin Street, Fifth Floor
22+
* Boston, MA 02110-1301 USA
23+
*/
24+
package org.hibernate.loader;
25+
26+
import org.jboss.logging.Logger;
27+
28+
/**
29+
* Defines the style that should be used to perform batch loading. Which style to use is declared using
30+
* the "{@value org.hibernate.cfg.AvailableSettings#BATCH_FETCH_STYLE}"
31+
* ({@link org.hibernate.cfg.AvailableSettings#BATCH_FETCH_STYLE}) setting
32+
*
33+
* @author Steve Ebersole
34+
*/
35+
public enum BatchFetchStyle {
36+
/**
37+
* The legacy algorithm where we keep a set of pre-built batch sizes based on
38+
* {@link org.hibernate.internal.util.collections.ArrayHelper#getBatchSizes}. Batches are performed
39+
* using the next-smaller pre-built batch size from the number of existing batchable identifiers.
40+
* <p/>
41+
* For example, with a batch-size setting of 32 the pre-built batch sizes would be [32, 16, 10, 9, 8, 7, .., 1].
42+
* An attempt to batch load 31 identifiers would result in batches of 16, 10, and 5.
43+
*/
44+
LEGACY,
45+
/**
46+
* Still keeps the concept of pre-built batch sizes, but uses the next-bigger batch size and pads the extra
47+
* identifier placeholders.
48+
* <p/>
49+
* Using the same example of a batch-size setting of 32 the pre-built batch sizes would be the same. However, the
50+
* attempt to batch load 31 identifiers would result just a single batch of size 32. The identifiers to load would
51+
* be "padded" (aka, repeated) to make up the difference.
52+
*/
53+
PADDED,
54+
/**
55+
* Dynamically builds its SQL based on the actual number of available ids. Does still limit to the batch-size
56+
* defined on the entity/collection
57+
*/
58+
DYNAMIC;
59+
60+
private static final Logger log = Logger.getLogger( BatchFetchStyle.class );
61+
62+
public static BatchFetchStyle byName(String name) {
63+
return valueOf( name.toUpperCase() );
64+
}
65+
66+
public static BatchFetchStyle interpret(Object setting) {
67+
log.tracef( "Interpreting BatchFetchStyle from setting : %s", setting );
68+
69+
if ( setting == null ) {
70+
return LEGACY; // as default
71+
}
72+
73+
if ( BatchFetchStyle.class.isInstance( setting ) ) {
74+
return (BatchFetchStyle) setting;
75+
}
76+
77+
try {
78+
final BatchFetchStyle byName = byName( setting.toString() );
79+
if ( byName != null ) {
80+
return byName;
81+
}
82+
}
83+
catch (Exception ignore) {
84+
}
85+
86+
log.debugf( "Unable to interpret given setting [%s] as BatchFetchStyle", setting );
87+
88+
return LEGACY; // again as default.
89+
}
90+
}

Diff for: hibernate-core/src/main/java/org/hibernate/loader/Loader.java

+13-4
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ public Loader(SessionFactoryImplementor factory) {
119119
*
120120
* @return The sql command this loader should use to get its {@link ResultSet}.
121121
*/
122-
protected abstract String getSQLString();
122+
public abstract String getSQLString();
123123

124124
/**
125125
* An array of persisters of entity classes contained in each row of results;
@@ -256,7 +256,7 @@ private String prependComment(String sql, QueryParameters parameters) {
256256
* persister from each row of the <tt>ResultSet</tt>. If an object is supplied, will attempt to
257257
* initialize that object. If a collection is supplied, attempt to initialize that collection.
258258
*/
259-
private List doQueryAndInitializeNonLazyCollections(
259+
public List doQueryAndInitializeNonLazyCollections(
260260
final SessionImplementor session,
261261
final QueryParameters queryParameters,
262262
final boolean returnProxies) throws HibernateException, SQLException {
@@ -268,7 +268,7 @@ private List doQueryAndInitializeNonLazyCollections(
268268
);
269269
}
270270

271-
private List doQueryAndInitializeNonLazyCollections(
271+
public List doQueryAndInitializeNonLazyCollections(
272272
final SessionImplementor session,
273273
final QueryParameters queryParameters,
274274
final boolean returnProxies,
@@ -1722,8 +1722,17 @@ protected ResultSet executeQueryStatement(
17221722
final QueryParameters queryParameters,
17231723
final boolean scroll,
17241724
final SessionImplementor session) throws SQLException {
1725+
return executeQueryStatement( getSQLString(), queryParameters, scroll, session );
1726+
}
1727+
1728+
protected ResultSet executeQueryStatement(
1729+
final String sqlStatement,
1730+
final QueryParameters queryParameters,
1731+
final boolean scroll,
1732+
final SessionImplementor session) throws SQLException {
1733+
17251734
// Processing query filters.
1726-
queryParameters.processFilters( getSQLString(), session );
1735+
queryParameters.processFilters( sqlStatement, session );
17271736

17281737
// Applying LIMIT clause.
17291738
final LimitHandler limitHandler = getLimitHandler(

0 commit comments

Comments
 (0)