Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions hibernate-core/src/main/java/org/hibernate/cfg/JdbcSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,21 @@ public interface JdbcSettings extends C3p0Settings, ProxoolSettings, AgroalSetti
*/
String ALLOW_METADATA_ON_BOOT = "hibernate.boot.allow_jdbc_metadata_access";

/**
* Whether failures to access the JDBC {@linkplain java.sql.DatabaseMetaData metadata} should be ignored and the
* bootstrapping process should continue. Hibernate then uses a default JDBC
* {@linkplain org.hibernate.engine.jdbc.env.spi.JdbcEnvironment environment}.
* <p/>
* Failures to access the metadata can result in unexpected runtime errors when accessing the database, since the
* default JDBC environment might not correctly represent the capabilities of the underlying database.
* <p/>
* This setting only takes effect when the {@link #ALLOW_METADATA_ON_BOOT} setting is activated.
*
* @settingDefault {@code true}
* @see #ALLOW_METADATA_ON_BOOT
* @since 6.6
*/
String IGNORE_METADATA_ACCESS_FAILURE_ON_BOOT = "hibernate.boot.ignore_jdbc_metadata_access_failure";

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Deprecated Hibernate settings
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@
import java.util.Map;
import java.util.StringTokenizer;

import org.hibernate.HibernateException;
import org.hibernate.boot.registry.StandardServiceInitiator;
import org.hibernate.cfg.JdbcSettings;
import org.hibernate.dialect.DatabaseVersion;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.config.spi.ConfigurationService;
import org.hibernate.engine.jdbc.batch.spi.BatchBuilder;
import org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator;
import org.hibernate.engine.jdbc.connections.internal.DatabaseConnectionInfoImpl;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.engine.jdbc.connections.spi.DatabaseConnectionInfo;
Expand Down Expand Up @@ -321,7 +321,7 @@ private JdbcEnvironmentImpl getJdbcEnvironmentUsingJdbcMetadata(
return temporaryJdbcSessionOwner.transactionCoordinator.createIsolationDelegate().delegateWork(
new AbstractReturningWork<>() {
@Override
public JdbcEnvironmentImpl execute(Connection connection) {
public JdbcEnvironmentImpl execute(Connection connection) throws SQLException {
try {
final DatabaseMetaData metadata = connection.getMetaData();
logDatabaseAndDriver( metadata );
Expand Down Expand Up @@ -357,7 +357,12 @@ public JdbcEnvironmentImpl execute(Connection connection) {
);
}
catch (SQLException e) {
log.unableToObtainConnectionMetadata( e );
if ( shouldIgnoreMetadataAccessFailure( configurationValues ) ) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case I added in MetadataAccessTests doesn't cover this case, since an actual JDBC connection is required and the Connection#getMetaData method needs to throw a SQLException to reach it. I didn't really know how to make this happen ☹️

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I achieved this by mocking the ConnectionProvider and the returned Connection, but this is pretty dependent on the actual implementation of the JdbcEnvironmentInitiator:

@Test
@Jira("https://hibernate.atlassian.net/browse/HHH-18286")
void testDontIgnoreMetadataAccessFailureWhenConnectionCanBeObtained() throws Exception {
	Connection connectionMock = Mockito.mock( Connection.class );
	given( connectionMock.getMetaData() ).willThrow( new SQLException( "Connection closed unexpectedly!" ) );

	ConnectionProvider connectionProviderMock = Mockito.mock( ConnectionProvider.class );
	given( connectionProviderMock.getConnection() ).willReturn( connectionMock );

	StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder();
	registryBuilder.clearSettings();
	registryBuilder.applySetting( JdbcSettings.IGNORE_METADATA_ACCESS_FAILURE_ON_BOOT, false )
			.applySetting( AvailableSettings.CONNECTION_PROVIDER, connectionProviderMock );

	try (StandardServiceRegistry registry = registryBuilder.build()) {
		assertThatExceptionOfType( ServiceException.class )
				.isThrownBy( () -> registry.getService( JdbcEnvironment.class ) )
				.withRootCauseInstanceOf( SQLException.class )
				.havingCause()
				.isInstanceOf( HibernateException.class )
				.withMessage( "Unable to access JDBC metadata" );
	}
}

The QualifiedTableNamingTest has a MockedConnectionProvider implementation of the ConnectionProvider interface, which uses the JdbcMocks factory class, but that uses dynamic proxies which do not throw a SQLException but actually return a result when calling the Connection#getMetaData method.

log.unableToObtainConnectionMetadata( e );
}
else {
throw e;
}
}

// accessing the JDBC metadata failed
Expand Down Expand Up @@ -387,12 +392,25 @@ private int databaseMicroVersion(DatabaseMetaData metadata) throws SQLException
);
}
catch ( Exception e ) {
log.unableToObtainConnectionToQueryMetadata( e );
if ( shouldIgnoreMetadataAccessFailure( configurationValues ) ) {
log.unableToObtainConnectionToQueryMetadata( e );
}
else {
throw new HibernateException( "Unable to access JDBC metadata", e );
}
}
// accessing the JDBC metadata failed
return getJdbcEnvironmentWithDefaults( configurationValues, registry, dialectFactory );
}

private static boolean shouldIgnoreMetadataAccessFailure(Map<String, Object> configurationValues) {
return getBoolean(
JdbcSettings.IGNORE_METADATA_ACCESS_FAILURE_ON_BOOT,
configurationValues,
true
);
}

private static void logDatabaseAndDriver(DatabaseMetaData dbmd) throws SQLException {
if ( log.isDebugEnabled() ) {
log.debugf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
*/
package org.hibernate.orm.test.boot.database.metadata;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hibernate.dialect.SimpleDatabaseVersion.ZERO_VERSION;
import static org.junit.jupiter.api.Assertions.fail;

import java.lang.reflect.Field;
import java.util.stream.Stream;

Expand Down Expand Up @@ -50,6 +46,11 @@

import org.jboss.logging.Logger;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.hibernate.dialect.SimpleDatabaseVersion.ZERO_VERSION;
import static org.junit.jupiter.api.Assertions.fail;

/**
* @author Steve Ebersole
*/
Expand Down Expand Up @@ -203,6 +204,23 @@ void testAccessDisabledNoDialectNorProductName() {
}
}

@Test
@Jira("https://hibernate.atlassian.net/browse/HHH-18286")
void testDontIgnoreMetadataAccessFailureWhenConnectionCantBeObtained() {
StandardServiceRegistryBuilder registryBuilder = new StandardServiceRegistryBuilder();
registryBuilder.clearSettings();

registryBuilder.applySetting( JdbcSettings.IGNORE_METADATA_ACCESS_FAILURE_ON_BOOT, false );

try (StandardServiceRegistry registry = registryBuilder.build()) {
assertThatExceptionOfType( ServiceException.class )
.isThrownBy( () -> registry.getService( JdbcEnvironment.class ) )
.havingCause()
.isInstanceOf( HibernateException.class )
.withMessage( "Unable to access JDBC metadata" );
}
}

// Ugly hack because neither MINIMUM_VERSION nor getMinimumSupportedVersion()
// can be accessed from this test.
private static DatabaseVersion getVersionConstant(Class<? extends Dialect> dialectClass, String versionConstantName) {
Expand Down