diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnectionPool.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnectionPool.java index 5abfe9293..ef2c95410 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnectionPool.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnectionPool.java @@ -6,6 +6,7 @@ package org.hibernate.reactive.pool; import org.hibernate.Incubating; +import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.reactive.provider.ReactiveServiceRegistryBuilder; import org.hibernate.service.Service; @@ -41,12 +42,27 @@ public interface ReactiveConnectionPool extends Service { */ CompletionStage getConnection(); + /** + * Obtain a reactive connection, returning the connection + * via a {@link CompletionStage} and overriding the default + * {@link SqlExceptionHelper} for the pool. + */ + CompletionStage getConnection(SqlExceptionHelper sqlExceptionHelper); + /** * Obtain a reactive connection for the given tenant id, * returning the connection via a {@link CompletionStage}. */ CompletionStage getConnection(String tenantId); + /** + * Obtain a reactive connection for the given tenant id, + * returning the connection via a {@link CompletionStage} + * and overriding the default {@link SqlExceptionHelper} + * for the pool. + */ + CompletionStage getConnection(String tenantId, SqlExceptionHelper sqlExceptionHelper); + /** * Obtain a lazily-initializing reactive connection. The * actual connection might be made when the returned diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPool.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPool.java index 529ba4bfc..35c3980e0 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPool.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/DefaultSqlClientPool.java @@ -14,6 +14,8 @@ import java.util.ServiceLoader; import java.util.concurrent.CompletionStage; +import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.internal.util.config.ConfigurationException; import org.hibernate.internal.util.config.ConfigurationHelper; @@ -109,6 +111,7 @@ public static VertxDriver findByClassName(String className) { private Pool pools; private SqlStatementLogger sqlStatementLogger; + private SqlExceptionHelper sqlExceptionHelper; private URI uri; private ServiceRegistryImplementor serviceRegistry; @@ -151,6 +154,15 @@ protected SqlStatementLogger getSqlStatementLogger() { return sqlStatementLogger; } + @Override + public SqlExceptionHelper getSqlExceptionHelper() { + if ( sqlExceptionHelper == null ) { + sqlExceptionHelper = serviceRegistry + .getService( JdbcServices.class ).getSqlExceptionHelper(); + } + return sqlExceptionHelper; + } + /** * Create a new {@link Pool} for the given JDBC URL or database URI, * using the {@link VertxInstance} service to obtain an instance of diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/ExternalSqlClientPool.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/ExternalSqlClientPool.java index 74185adf3..e88f9c5a7 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/ExternalSqlClientPool.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/ExternalSqlClientPool.java @@ -7,6 +7,7 @@ import java.util.concurrent.CompletionStage; +import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.stage.Stage; @@ -47,10 +48,12 @@ public final class ExternalSqlClientPool extends SqlClientPool { private final Pool pool; private final SqlStatementLogger sqlStatementLogger; + private SqlExceptionHelper sqlExceptionHelper; - public ExternalSqlClientPool(Pool pool, SqlStatementLogger sqlStatementLogger) { + public ExternalSqlClientPool(Pool pool, SqlStatementLogger sqlStatementLogger, SqlExceptionHelper sqlExceptionHelper) { this.pool = pool; this.sqlStatementLogger = sqlStatementLogger; + this.sqlExceptionHelper = sqlExceptionHelper; } @Override @@ -63,13 +66,17 @@ protected SqlStatementLogger getSqlStatementLogger() { return sqlStatementLogger; } + @Override + public SqlExceptionHelper getSqlExceptionHelper() { + return sqlExceptionHelper; + } + /** * Since this Service implementation does not implement @{@link org.hibernate.service.spi.Stoppable} * and we're only adapting an externally provided pool, we will not actually close such provided pool * when Hibernate ORM is shutdown (it doesn't own the lifecycle of this external component). - * Therefore there is no need to wait for its shutdown and this method returns an already + * Therefore, there is no need to wait for its shutdown and this method returns an already * successfully completed CompletionStage. - * @return */ @Override public CompletionStage getCloseFuture() { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java index 9b2cf7db4..adb684137 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java @@ -14,6 +14,7 @@ import java.util.concurrent.CompletionStage; import org.hibernate.engine.jdbc.internal.FormatStyle; +import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.reactive.adaptor.impl.JdbcNull; import org.hibernate.reactive.adaptor.impl.ResultSetAdaptor; @@ -25,6 +26,7 @@ import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; +import io.vertx.sqlclient.DatabaseException; import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.PrepareOptions; import io.vertx.sqlclient.PropertyKind; @@ -53,15 +55,17 @@ public class SqlClientConnection implements ReactiveConnection { private final static PropertyKind ORACLE_GENERATED_KEYS = PropertyKind.create( "generated-keys", Row.class ); private final SqlStatementLogger sqlStatementLogger; + private final SqlExceptionHelper sqlExceptionHelper; private final Pool pool; private final SqlConnection connection; private Transaction transaction; - SqlClientConnection(SqlConnection connection, Pool pool, SqlStatementLogger sqlStatementLogger) { + SqlClientConnection(SqlConnection connection, Pool pool, SqlStatementLogger sqlStatementLogger, SqlExceptionHelper sqlExceptionHelper) { this.pool = pool; this.sqlStatementLogger = sqlStatementLogger; this.connection = connection; + this.sqlExceptionHelper = sqlExceptionHelper; LOG.tracef( "Connection created: %s", connection ); } @@ -151,6 +155,11 @@ private T convertException(T rows, String sql, Throwable sqlException) { if ( sqlException == null ) { return rows; } + if ( sqlException instanceof DatabaseException ) { + DatabaseException de = (DatabaseException) sqlException; + sqlException = sqlExceptionHelper + .convert( new SQLException( de.getMessage(), de.getSqlState(), de.getErrorCode() ), "error executing SQL statement", sql ); + } return rethrow( sqlException ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientPool.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientPool.java index af7da89a6..54cb35faf 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientPool.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientPool.java @@ -7,6 +7,7 @@ import java.util.concurrent.CompletionStage; +import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.reactive.pool.ReactiveConnection; import org.hibernate.reactive.pool.ReactiveConnectionPool; @@ -44,6 +45,12 @@ public abstract class SqlClientPool implements ReactiveConnectionPool { */ protected abstract SqlStatementLogger getSqlStatementLogger(); + /** + * @return a Hibernate {@link SqlExceptionHelper} for converting + * exceptions + */ + protected abstract SqlExceptionHelper getSqlExceptionHelper(); + /** * Get a {@link Pool} for the specified tenant. *

@@ -57,7 +64,7 @@ public abstract class SqlClientPool implements ReactiveConnectionPool { * @see ReactiveConnectionPool#getConnection(String) */ protected Pool getTenantPool(String tenantId) { - throw new UnsupportedOperationException("multitenancy not supported by built-in SqlClientPool"); + throw new UnsupportedOperationException( "multitenancy not supported by built-in SqlClientPool" ); } @Override @@ -65,18 +72,37 @@ public CompletionStage getConnection() { return getConnectionFromPool( getPool() ); } + @Override + public CompletionStage getConnection(SqlExceptionHelper sqlExceptionHelper) { + return getConnectionFromPool( getPool(), sqlExceptionHelper ); + } + @Override public CompletionStage getConnection(String tenantId) { return getConnectionFromPool( getTenantPool( tenantId ) ); } + @Override + public CompletionStage getConnection(String tenantId, SqlExceptionHelper sqlExceptionHelper) { + return getConnectionFromPool( getTenantPool( tenantId ), sqlExceptionHelper ); + } + private CompletionStage getConnectionFromPool(Pool pool) { return pool.getConnection() .toCompletionStage().thenApply( this::newConnection ); } + private CompletionStage getConnectionFromPool(Pool pool, SqlExceptionHelper sqlExceptionHelper) { + return pool.getConnection() + .toCompletionStage().thenApply( sqlConnection -> newConnection( sqlConnection, sqlExceptionHelper ) ); + } + private SqlClientConnection newConnection(SqlConnection connection) { - return new SqlClientConnection( connection, getPool(), getSqlStatementLogger() ); + return newConnection( connection, getSqlExceptionHelper() ); + } + + private SqlClientConnection newConnection(SqlConnection connection, SqlExceptionHelper sqlExceptionHelper) { + return new SqlClientConnection( connection, getPool(), getSqlStatementLogger(), sqlExceptionHelper ); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java index 012bd62b7..e30730efc 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java @@ -15,6 +15,7 @@ import org.hibernate.engine.jdbc.dialect.spi.DialectFactory; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.reactive.engine.jdbc.env.internal.ReactiveJdbcEnvironment; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LogCategory; @@ -27,9 +28,9 @@ import io.vertx.sqlclient.spi.DatabaseMetadata; +import static java.util.function.Function.identity; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; -import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; /** * A Hibernate {@link StandardServiceInitiator service initiator} that @@ -96,27 +97,31 @@ private static Dialect checkDialect(Dialect dialect) { private DialectResolutionInfo dialectResolutionInfo() { ReactiveConnectionPool connectionPool = registry.getService( ReactiveConnectionPool.class ); - return connectionPool.getConnection() - .thenCompose( DialectBuilder::buildResolutionInfo ).toCompletableFuture().join(); + return connectionPool + // The default SqlExceptionHelper in ORM requires the dialect, but we haven't create a dialect yet + // so we need to override it at this stage, or we will have an exception. + .getConnection( new SqlExceptionHelper( true ) ) + .thenCompose( DialectBuilder::buildResolutionInfo ) + .toCompletableFuture().join(); } private static CompletionStage buildResolutionInfo(ReactiveConnection connection) { - try { - final DatabaseMetadata databaseMetadata = connection.getDatabaseMetadata(); - return resolutionInfoStage( connection, databaseMetadata ) - .thenCompose( info -> connection.close().thenApply( v -> info ) ); - } - catch (Throwable t) { - try { - return connection.close() - .handle( CompletionStages::handle ) - // Ignore errors when closing the connection - .thenCompose( handled -> failedFuture( t ) ); - } - catch (Throwable onClose) { - return failedFuture( t ); - } - } + final DatabaseMetadata databaseMetadata = connection.getDatabaseMetadata(); + return resolutionInfoStage( connection, databaseMetadata ) + .handle( CompletionStages::handle ) + .thenCompose( handled -> { + if ( handled.hasFailed() ) { + // Something has already gone wrong: try to close the connection + // and return the original failure + return connection.close() + .handle( (unused, throwable) -> handled.getResultAsCompletionStage() ) + .thenCompose( identity() ); + } + else { + return connection.close() + .thenCompose( v -> handled.getResultAsCompletionStage() ); + } + } ); } private static CompletionStage resolutionInfoStage(ReactiveConnection connection, DatabaseMetadata databaseMetadata) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java index e17a25dd8..215602892 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/util/impl/CompletionStages.java @@ -243,6 +243,14 @@ public CompletionStageHandler(R result, T throwable) { this.throwable = throwable; } + public boolean hasFailed() { + return throwable != null; + } + + public T getThrowable() { + return throwable; + } + public R getResult() throws T { if ( throwable == null ) { return result; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyExceptionsTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyExceptionsTest.java index c51b9dc34..9ffce3b3d 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyExceptionsTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinyExceptionsTest.java @@ -8,7 +8,7 @@ import java.util.Collection; import java.util.List; -import org.hibernate.HibernateException; +import org.hibernate.exception.ConstraintViolationException; import org.hibernate.reactive.mutiny.Mutiny; import org.junit.Test; @@ -27,7 +27,7 @@ protected Collection> annotatedEntities() { } Class getExpectedException() { - return HibernateException.class; + return ConstraintViolationException.class; } @Test diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveConstraintViolationTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveConstraintViolationTest.java new file mode 100644 index 000000000..272650fbe --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveConstraintViolationTest.java @@ -0,0 +1,103 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletionStage; + +import org.hibernate.exception.ConstraintViolationException; + +import org.junit.Test; + +import io.vertx.ext.unit.TestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +import static org.hibernate.reactive.testing.ReactiveAssertions.assertThrown; + +public class ReactiveConstraintViolationTest extends BaseReactiveTest { + + @Override + protected Set> annotatedEntities() { + return Set.of( GuineaPig.class ); + } + + private CompletionStage populateDB() { + return getSessionFactory() + .withTransaction( s -> s.persist( new GuineaPig( 5, "Aloi" ) ) ); + } + + @Test + public void reactiveConstraintViolation(TestContext context) { + test( context, assertThrown( + ConstraintViolationException.class, + populateDB() + .thenCompose( v -> openSession() ) + .thenCompose( s -> s.persist( new GuineaPig( 5, "Aloi" ) ) + .thenCompose( i -> s.flush() ) ) + ) + ); + } + + @Entity(name = "GuineaPig") + @Table(name = "bad_pig") + public static class GuineaPig { + @Id + private Integer id; + private String name; + @Version + private int version; + + public GuineaPig() { + } + + public GuineaPig(Integer id, String name) { + this.id = id; + this.name = 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; + } + + @Override + public String toString() { + return id + ": " + name; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + GuineaPig guineaPig = (GuineaPig) o; + return Objects.equals( name, guineaPig.name ); + } + + @Override + public int hashCode() { + return Objects.hash( name ); + } + } +}