Skip to content
50 changes: 50 additions & 0 deletions hibernate-core/src/main/java/org/hibernate/BatchSize.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate;

import jakarta.persistence.FindOption;

import java.util.List;

/**
* Specify a batch size, that is, how many entities should be
* fetched in each request to the database, for an invocation of
* {@link Session#findMultiple(Class, List, FindOption...)}.
* <ul>
* <li>By default, the batch sizing strategy is determined by the
* {@linkplain org.hibernate.dialect.Dialect#getBatchLoadSizingStrategy
* SQL dialect}, but
* <li>if some {@code batchSize>1} is specified as an
* argument to this method, then that batch size will be used.
* </ul>
* <p>
* If an explicit batch size is set manually, care should be taken
* to not exceed the capabilities of the underlying database.
* <p>
* The performance impact of setting a batch size depends on whether
* a SQL array may be used to pass the list of identifiers to the
* database:
* <ul>
* <li>for databases which support standard SQL arrays, a smaller
* batch size might be extremely inefficient compared to a very
* large batch size or no batching at all, but
* <li>on the other hand, for databases with no SQL array type, a
* large batch size results in long SQL statements with many JDBC
* parameters.
* <p>
* A batch size is considered a hint. This option has no effect
* on {@link Session#find(Class, Object, FindOption...)}.
*
* @param batchSize The batch size
*
* @see Session#findMultiple
* @see MultiIdentifierLoadAccess#withBatchSize
*
* @since 7.0
*
* @author Gavin King
*/
public record BatchSize(int batchSize) implements FindOption {
}
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ default MultiIdentifierLoadAccess<T> with(RootGraph<T> graph) {
* If an explicit batch size is set manually, care should be taken
* to not exceed the capabilities of the underlying database.
* <p>
* The performance impact of setting a batch size depends on whether
* a SQL array may be used to pass the list of identifiers to the
* database:
* <ul>
* <li>for databases which support standard SQL arrays, a smaller
* batch size might be extremely inefficient compared to a very
* large batch size or no batching at all, but
* <li>on the other hand, for databases with no SQL array type, a
* large batch size results in long SQL statements with many JDBC
* parameters.
* </ul>
* <p>
* A batch size is considered a hint.
*
* @param batchSize The batch size
Expand Down
12 changes: 12 additions & 0 deletions hibernate-core/src/main/java/org/hibernate/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,18 @@ public interface Session extends SharedSessionContract, EntityManager {
* Every object returned by {@code findMultiple()} is either an unproxied instance of the
* given entity class, or a fully-fetched proxy object.
* <p>
* This method accepts {@link BatchSize} as an option, allowing control over the number of
* records retrieved in a single database request. The performance impact of setting a batch
* size depends on whether a SQL array may be used to pass the list of identifiers to the
* database:
* <ul>
* <li>for databases which {@linkplain org.hibernate.dialect.Dialect#supportsStandardArrays
* support standard SQL arrays}, a smaller batch size might be extremely inefficient
* compared to a very large batch size or no batching at all, but
* <li>on the other hand, for databases with no SQL array type, a large batch size results
* in long SQL statements with many JDBC parameters.
* </ul>
* <p>
* For more advanced cases, use {@link #byMultipleIds(Class)}, which returns an instance of
* {@link MultiIdentifierLoadAccess}.
*
Expand Down
22 changes: 7 additions & 15 deletions hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@
import org.hibernate.exception.spi.SQLExceptionConverter;
import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
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.loader.ast.spi.MultiKeyLoadSizingStrategy;
Expand Down Expand Up @@ -208,6 +207,7 @@
import static org.hibernate.cfg.AvailableSettings.NON_CONTEXTUAL_LOB_CREATION;
import static org.hibernate.cfg.AvailableSettings.STATEMENT_BATCH_SIZE;
import static org.hibernate.cfg.AvailableSettings.USE_GET_GENERATED_KEYS;
import static org.hibernate.internal.util.MathHelper.ceilingPowerOfTwo;
import static org.hibernate.internal.util.StringHelper.splitAtCommas;
import static org.hibernate.internal.util.collections.ArrayHelper.EMPTY_STRING_ARRAY;
import static org.hibernate.type.SqlTypes.*;
Expand Down Expand Up @@ -4385,21 +4385,13 @@ public MultiKeyLoadSizingStrategy getBatchLoadSizingStrategy() {
return getMultiKeyLoadSizingStrategy();
}

protected final MultiKeyLoadSizingStrategy STANDARD_MULTI_KEY_LOAD_SIZING_STRATEGY = (numberOfColumns, numberOfKeys, pad) -> {
numberOfKeys = pad ? MathHelper.ceilingPowerOfTwo( numberOfKeys ) : numberOfKeys;

final long parameterCount = (long) numberOfColumns * numberOfKeys;
final int limit = getParameterCountLimit();

if ( limit > 0 ) {
// the Dialect reported a limit - see if the parameter count exceeds the limit
if ( parameterCount >= limit ) {
return limit / numberOfColumns;
}
}
private int calculateBatchSize(int numberOfColumns, int numberOfKeys, boolean padToPowerOfTwo) {
final int batchSize = padToPowerOfTwo ? ceilingPowerOfTwo( numberOfKeys ) : numberOfKeys;
final int maxBatchSize = getParameterCountLimit() / numberOfColumns;
return maxBatchSize > 0 && batchSize > maxBatchSize ? maxBatchSize : batchSize;
}

return numberOfKeys;
};
protected final MultiKeyLoadSizingStrategy STANDARD_MULTI_KEY_LOAD_SIZING_STRATEGY = this::calculateBatchSize;

/**
* Is JDBC statement warning logging enabled by default?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.exception;

import org.hibernate.JDBCException;

import java.sql.SQLException;

/**
* A {@link JDBCException} indicating an authentication or authorization failure.
*
* @since 7.0
*
* @author Gavin King
*/
public class AuthException extends JDBCException {
/**
* Constructor for AuthException.
*
* @param root The underlying exception.
*/
public AuthException(String message, SQLException root) {
super( message, root );
}

/**
* Constructor for AuthException.
*
* @param message Optional message.
* @param root The underlying exception.
*/
public AuthException(String message, SQLException root, String sql) {
super( message, root, sql );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
import org.hibernate.JDBCException;

/**
* Extends {@link JDBCException} indicating that evaluation of the
* A {@link JDBCException} indicating that evaluation of the
* valid SQL statement against the given data resulted in some
* illegal operation, mismatched types or incorrect cardinality.
*
* @author Gavin King
*/
public class DataException extends JDBCException {
/**
* Constructor for JDBCException.
* Constructor for DataException.
*
* @param root The underlying exception.
*/
Expand All @@ -26,7 +26,7 @@ public DataException(String message, SQLException root) {
}

/**
* Constructor for JDBCException.
* Constructor for DataException.
*
* @param message Optional message.
* @param root The underlying exception.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,120 +5,95 @@
package org.hibernate.exception.internal;

import java.sql.SQLException;
import java.util.Set;

import org.hibernate.JDBCException;
import org.hibernate.PessimisticLockException;
import org.hibernate.QueryTimeoutException;
import org.hibernate.exception.AuthException;
import org.hibernate.exception.ConstraintViolationException;
import org.hibernate.exception.DataException;
import org.hibernate.exception.JDBCConnectionException;
import org.hibernate.exception.LockAcquisitionException;
import org.hibernate.exception.SQLGrammarException;
import org.hibernate.exception.spi.AbstractSQLExceptionConversionDelegate;
import org.hibernate.exception.spi.ConversionContext;
import org.hibernate.internal.util.JdbcExceptionHelper;

import org.checkerframework.checker.nullness.qual.Nullable;

import static org.hibernate.internal.util.JdbcExceptionHelper.determineSqlStateClassCode;
import static org.hibernate.internal.util.JdbcExceptionHelper.extractErrorCode;
import static org.hibernate.internal.util.JdbcExceptionHelper.extractSqlState;

/**
* A {@link org.hibernate.exception.spi.SQLExceptionConverter} implementation which performs conversion based
* on the underlying SQLState. Interpretation of a SQL error based on SQLState is not nearly as accurate as
* using the ErrorCode (which is, however, vendor-specific).
* <p>
* SQLState codes are defined by both ANSI SQL specs and X/Open. Some "classes" are shared, others are
* specific to one or another, yet others are custom vendor classes. Unfortunately I have not been able to
* find a "blessed" list of X/Open codes. These codes are cobbled together between ANSI SQL spec and error
*
* @implNote
* SQLState codes are defined by both ANSI SQL specs and X/Open. Some "classes" are shared, others are
* specific to one or another, yet others are custom vendor classes. Unfortunately I have not been able to
* find a "blessed" list of X/Open codes. These codes are cobbled together between ANSI SQL spec and error
* code tables from few vendor's documentation.
*
* @author Steve Ebersole
*/
public class SQLStateConversionDelegate extends AbstractSQLExceptionConversionDelegate {

private static final Set<String> SQL_GRAMMAR_CATEGORIES = buildGrammarCategories();
private static Set<String> buildGrammarCategories() {
return Set.of(
"07", // "dynamic SQL error"
"20",
"2A", // "direct SQL syntax error or access rule violation"
"37", // "dynamic SQL syntax error or access rule violation"
"42", // "syntax error or access rule violation"
"65", // Oracle specific as far as I can tell
"S0" // MySQL specific as far as I can tell
);
}

private static final Set<String> DATA_CATEGORIES = buildDataCategories();
private static Set<String> buildDataCategories() {
return Set.of(
"21", // "cardinality violation"
"22" // "data exception"
);
}

private static final Set<String> INTEGRITY_VIOLATION_CATEGORIES = buildContraintCategories();
private static Set<String> buildContraintCategories() {
return Set.of(
"23", // "integrity constraint violation"
"27", // "triggered data change violation"
"44" // "with check option violation"
);
}

private static final Set<String> CONNECTION_CATEGORIES = buildConnectionCategories();
private static Set<String> buildConnectionCategories() {
return Set.of(
"08" // "connection exception"
);
}

public SQLStateConversionDelegate(ConversionContext conversionContext) {
super( conversionContext );
}

@Override
public @Nullable JDBCException convert(SQLException sqlException, String message, String sql) {
final String sqlState = JdbcExceptionHelper.extractSqlState( sqlException );
final int errorCode = JdbcExceptionHelper.extractErrorCode( sqlException );

final String sqlState = extractSqlState( sqlException );
if ( sqlState != null ) {
String sqlStateClassCode = JdbcExceptionHelper.determineSqlStateClassCode( sqlState );

if ( sqlStateClassCode != null ) {
if ( SQL_GRAMMAR_CATEGORIES.contains( sqlStateClassCode ) ) {
switch ( sqlState ) {
case "42501":
return new AuthException( message, sqlException, sql );
case "40001":
return new LockAcquisitionException( message, sqlException, sql );
case "40XL1", "40XL2":
// Derby "A lock could not be obtained within the time requested."
return new PessimisticLockException( message, sqlException, sql );
case "70100":
// MySQL Query execution was interrupted
return new QueryTimeoutException( message, sqlException, sql );
case "72000":
if ( extractErrorCode( sqlException ) == 1013 ) {
// Oracle user requested cancel of current operation
return new QueryTimeoutException( message, sqlException, sql );
}
}
switch ( determineSqlStateClassCode( sqlState ) ) {
case
"07", // "dynamic SQL error"
"20",
"2A", // "direct SQL syntax error or access rule violation"
"37", // "dynamic SQL syntax error or access rule violation"
"42", // "syntax error or access rule violation"
"65", // Oracle specific as far as I can tell
"S0": // MySQL specific as far as I can tell
return new SQLGrammarException( message, sqlException, sql );
}
else if ( INTEGRITY_VIOLATION_CATEGORIES.contains( sqlStateClassCode ) ) {
case
"23", // "integrity constraint violation"
"27", // "triggered data change violation"
"44": // "with check option violation"
final String constraintName = getConversionContext()
.getViolatedConstraintNameExtractor()
.extractConstraintName( sqlException );
return new ConstraintViolationException( message, sqlException, sql, constraintName );
}
else if ( CONNECTION_CATEGORIES.contains( sqlStateClassCode ) ) {
case
"08": // "connection exception"
return new JDBCConnectionException( message, sqlException, sql );
}
else if ( DATA_CATEGORIES.contains( sqlStateClassCode ) ) {
case
"21", // "cardinality violation"
"22": // "data exception"
return new DataException( message, sqlException, sql );
}
}

if ( "40001".equals( sqlState ) ) {
return new LockAcquisitionException( message, sqlException, sql );
}

if ( "40XL1".equals( sqlState ) || "40XL2".equals( sqlState )) {
// Derby "A lock could not be obtained within the time requested."
return new PessimisticLockException( message, sqlException, sql );
}

// MySQL Query execution was interrupted
if ( "70100".equals( sqlState ) ||
// Oracle user requested cancel of current operation
( "72000".equals( sqlState ) && errorCode == 1013 ) ) {
return new QueryTimeoutException( message, sqlException, sql );
case
"28": // "authentication failure"
return new AuthException( message, sqlException, sql );
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import jakarta.persistence.PessimisticLockScope;
import jakarta.persistence.Timeout;
import org.hibernate.BatchSize;
import org.hibernate.CacheMode;
import org.hibernate.ConnectionAcquisitionMode;
import org.hibernate.EntityFilterException;
Expand Down Expand Up @@ -950,6 +951,7 @@ private <T> MultiIdentifierLoadAccess<T> multiloadAccessWithOptions(Class<T> ent
CacheStoreMode storeMode = getCacheStoreMode();
CacheRetrieveMode retrieveMode = getCacheRetrieveMode();
LockOptions lockOptions = copySessionLockOptions();
int batchSize = -1;
for ( FindOption option : options ) {
if ( option instanceof CacheStoreMode cacheStoreMode ) {
storeMode = cacheStoreMode;
Expand Down Expand Up @@ -982,8 +984,13 @@ else if ( option instanceof EnabledFetchProfile enabledFetchProfile ) {
else if ( option instanceof ReadOnlyMode ) {
loadAccess.withReadOnly( option == ReadOnlyMode.READ_ONLY );
}
else if ( option instanceof BatchSize batchSizeOption ) {
batchSize = batchSizeOption.batchSize();
}
}
loadAccess.with( lockOptions ).with( interpretCacheMode( storeMode, retrieveMode ) );
loadAccess.with( lockOptions )
.with( interpretCacheMode( storeMode, retrieveMode ) )
.withBatchSize( batchSize );
return loadAccess;
}

Expand Down
Loading
Loading