diff --git a/hibernate-core/src/main/java/org/hibernate/boot/SessionFactoryBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/SessionFactoryBuilder.java index 7ca0666576db..0d86037ae7f4 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/SessionFactoryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/SessionFactoryBuilder.java @@ -15,6 +15,7 @@ import org.hibernate.annotations.CacheLayout; import org.hibernate.cache.spi.TimestampsCacheFactory; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.context.spi.TenantSchemaMapper; import org.hibernate.jpa.spi.JpaCompliance; import org.hibernate.proxy.EntityNotFoundDelegate; import org.hibernate.query.sqm.function.SqmFunctionDescriptor; @@ -384,6 +385,21 @@ public interface SessionFactoryBuilder { */ SessionFactoryBuilder applyCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver resolver); + /** + * Specifies a {@link TenantSchemaMapper} that is responsible for + * mapping the current tenant identifier to the name of a database + * schema. + * + * @param mapper The mapping strategy to use. + * + * @return {@code this}, for method chaining + * + * @see org.hibernate.cfg.AvailableSettings#MULTI_TENANT_SCHEMA_MAPPER + * + * @since 7.1 + */ + SessionFactoryBuilder applyTenantSchemaMapper(TenantSchemaMapper mapper); + /** * If using the built-in JTA-based * {@link org.hibernate.resource.transaction.spi.TransactionCoordinator} or diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryBuilderImpl.java index 762358c00be2..c73391ebe3a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryBuilderImpl.java @@ -23,6 +23,7 @@ import org.hibernate.bytecode.spi.BytecodeProvider; import org.hibernate.cache.spi.TimestampsCacheFactory; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.context.spi.TenantSchemaMapper; import org.hibernate.internal.SessionFactoryImpl; import org.hibernate.proxy.EntityNotFoundDelegate; import org.hibernate.query.sqm.function.SqmFunctionDescriptor; @@ -252,6 +253,12 @@ public SessionFactoryBuilder applyCurrentTenantIdentifierResolver(CurrentTenantI return this; } + @Override + public SessionFactoryBuilder applyTenantSchemaMapper(TenantSchemaMapper mapper) { + this.optionsBuilder.applyTenantSchemaMapper( mapper ); + return this; + } + @Override public SessionFactoryBuilder applyNamedQueryCheckingOnStartup(boolean enabled) { this.optionsBuilder.enableNamedQueryCheckingOnStartup( enabled ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java index 642449a502fa..5473dc091fc9 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java @@ -28,6 +28,7 @@ import org.hibernate.LockOptions; import org.hibernate.SessionEventListener; import org.hibernate.SessionFactoryObserver; +import org.hibernate.context.spi.TenantSchemaMapper; import org.hibernate.type.TimeZoneStorageStrategy; import org.hibernate.annotations.CacheLayout; import org.hibernate.boot.SchemaAutoTooling; @@ -187,6 +188,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions { // multi-tenancy private boolean multiTenancyEnabled; private CurrentTenantIdentifierResolver currentTenantIdentifierResolver; + private TenantSchemaMapper tenantSchemaMapper; // Queries private SqmFunctionRegistry sqmFunctionRegistry; @@ -371,6 +373,9 @@ public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, Boo null ); } + tenantSchemaMapper = + strategySelector.resolveStrategy( TenantSchemaMapper.class, + settings.get( MULTI_TENANT_SCHEMA_MAPPER ) ); delayBatchFetchLoaderCreations = configurationService.getSetting( DELAY_ENTITY_LOADER_CREATIONS, BOOLEAN, true ); @@ -1003,6 +1008,11 @@ public boolean isMultiTenancyEnabled() { return multiTenancyEnabled; } + @Override + public TenantSchemaMapper getTenantSchemaMapper() { + return tenantSchemaMapper; + } + @Override public CurrentTenantIdentifierResolver getCurrentTenantIdentifierResolver() { return currentTenantIdentifierResolver; @@ -1450,6 +1460,11 @@ public void applyCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver this.currentTenantIdentifierResolver = (CurrentTenantIdentifierResolver) resolver; } + public void applyTenantSchemaMapper(TenantSchemaMapper mapper) { + //noinspection unchecked + this.tenantSchemaMapper = (TenantSchemaMapper) mapper; + } + public void enableNamedQueryCheckingOnStartup(boolean enabled) { this.namedQueryStartupCheckingEnabled = enabled; } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryBuilder.java index 657d62ba8b3c..3f82b67a1177 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryBuilder.java @@ -16,6 +16,7 @@ import org.hibernate.boot.TempTableDdlTransactionHandling; import org.hibernate.cache.spi.TimestampsCacheFactory; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.context.spi.TenantSchemaMapper; import org.hibernate.proxy.EntityNotFoundDelegate; import org.hibernate.query.sqm.function.SqmFunctionDescriptor; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; @@ -208,6 +209,12 @@ public T applyCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver return getThis(); } + @Override + public SessionFactoryBuilder applyTenantSchemaMapper(TenantSchemaMapper mapper) { + delegate.applyTenantSchemaMapper( mapper ); + return getThis(); + } + @Override public T applyJtaTrackingByThread(boolean enabled) { delegate.applyJtaTrackingByThread( enabled ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java index 6b86f67d8387..3ec439e03428 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/AbstractDelegatingSessionFactoryOptions.java @@ -17,6 +17,7 @@ import org.hibernate.Interceptor; import org.hibernate.LockOptions; import org.hibernate.SessionFactoryObserver; +import org.hibernate.context.spi.TenantSchemaMapper; import org.hibernate.type.TimeZoneStorageStrategy; import org.hibernate.annotations.CacheLayout; import org.hibernate.boot.SchemaAutoTooling; @@ -224,6 +225,11 @@ public CurrentTenantIdentifierResolver getCurrentTenantIdentifierResolve return delegate.getCurrentTenantIdentifierResolver(); } + @Override + public TenantSchemaMapper getTenantSchemaMapper() { + return delegate.getTenantSchemaMapper(); + } + @Override public JavaType getDefaultTenantIdentifierJavaType() { return delegate.getDefaultTenantIdentifierJavaType(); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java index 0ae58214f235..63ae89317a6d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/spi/SessionFactoryOptions.java @@ -20,6 +20,7 @@ import org.hibernate.LockOptions; import org.hibernate.SessionEventListener; import org.hibernate.SessionFactoryObserver; +import org.hibernate.context.spi.TenantSchemaMapper; import org.hibernate.type.TimeZoneStorageStrategy; import org.hibernate.annotations.CacheLayout; import org.hibernate.boot.SchemaAutoTooling; @@ -313,6 +314,18 @@ default SessionEventListener[] buildSessionEventListeners() { */ CurrentTenantIdentifierResolver getCurrentTenantIdentifierResolver(); + /** + * Obtain a reference to the current {@linkplain TenantSchemaMapper tenant schema mapper}, + * which is used to {@linkplain java.sql.Connection#setSchema set the schema} to the + * {@linkplain TenantSchemaMapper#schemaName schema belonging to the current tenant} + * each time a connection is obtained. + * + * @see org.hibernate.cfg.MultiTenancySettings#MULTI_TENANT_SCHEMA_MAPPER + * + * @since 7.1 + */ + TenantSchemaMapper getTenantSchemaMapper(); + /** * @see org.hibernate.cfg.TransactionSettings#JTA_TRACK_BY_THREAD */ diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/Configuration.java b/hibernate-core/src/main/java/org/hibernate/cfg/Configuration.java index 30bac56e51ae..9bfc079ae81e 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/Configuration.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/Configuration.java @@ -48,6 +48,7 @@ import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.boot.spi.XmlMappingBinderAccess; import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.context.spi.TenantSchemaMapper; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.EmptyInterceptor; @@ -173,7 +174,8 @@ public class Configuration { private EntityNotFoundDelegate entityNotFoundDelegate; private SessionFactoryObserver sessionFactoryObserver; private StatementInspector statementInspector; - private CurrentTenantIdentifierResolver currentTenantIdentifierResolver; + private CurrentTenantIdentifierResolver currentTenantIdentifierResolver; + private TenantSchemaMapper tenantSchemaMapper; private CustomEntityDirtinessStrategy customEntityDirtinessStrategy; private ColumnOrderingStrategy columnOrderingStrategy; private SharedCacheMode sharedCacheMode; @@ -939,7 +941,7 @@ public Configuration setStatementInspector(StatementInspector statementInspector /** * The {@link CurrentTenantIdentifierResolver}, if any, that was added to this configuration. */ - public CurrentTenantIdentifierResolver getCurrentTenantIdentifierResolver() { + public CurrentTenantIdentifierResolver getCurrentTenantIdentifierResolver() { return currentTenantIdentifierResolver; } @@ -948,11 +950,32 @@ public CurrentTenantIdentifierResolver getCurrentTenantIdentifierResolve * * @return {@code this} for method chaining */ - public Configuration setCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver currentTenantIdentifierResolver) { + public Configuration setCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver currentTenantIdentifierResolver) { this.currentTenantIdentifierResolver = currentTenantIdentifierResolver; return this; } + /** + * The {@link TenantSchemaMapper}, if any, that was added to this configuration. + * + * @since 7.1 + */ + public TenantSchemaMapper getTenantSchemaMapper() { + return tenantSchemaMapper; + } + + /** + * Specify a {@link TenantSchemaMapper} to be added to this configuration. + * + * @return {@code this} for method chaining + * + * @since 7.1 + */ + public Configuration setTenantSchemaMapper(TenantSchemaMapper tenantSchemaMapper) { + this.tenantSchemaMapper = tenantSchemaMapper; + return this; + } + /** * The {@link CustomEntityDirtinessStrategy}, if any, that was added to this configuration. */ @@ -1082,6 +1105,10 @@ public SessionFactory buildSessionFactory(ServiceRegistry serviceRegistry) throw sessionFactoryBuilder.applyCurrentTenantIdentifierResolver( currentTenantIdentifierResolver ); } + if ( tenantSchemaMapper != null ) { + sessionFactoryBuilder.applyTenantSchemaMapper( tenantSchemaMapper ); + } + if ( customEntityDirtinessStrategy != null ) { sessionFactoryBuilder.applyCustomEntityDirtinessStrategy( customEntityDirtinessStrategy ); } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/MultiTenancySettings.java b/hibernate-core/src/main/java/org/hibernate/cfg/MultiTenancySettings.java index d8d05486676c..70e4a16d21ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/MultiTenancySettings.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/MultiTenancySettings.java @@ -22,24 +22,44 @@ public interface MultiTenancySettings { String MULTI_TENANT_CONNECTION_PROVIDER = "hibernate.multi_tenant_connection_provider"; /** - * Specifies a {@link CurrentTenantIdentifierResolver} to use, - * either: + * Specifies a {@link CurrentTenantIdentifierResolver} to use, either: *
    *
  • an instance of {@code CurrentTenantIdentifierResolver}, - *
  • a {@link Class} representing an class that implements {@code CurrentTenantIdentifierResolver}, or + *
  • a {@link Class} representing a class that implements {@code CurrentTenantIdentifierResolver}, or *
  • the name of a class that implements {@code CurrentTenantIdentifierResolver}. *
* - * @see org.hibernate.boot.SessionFactoryBuilder#applyCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver) + * @see CurrentTenantIdentifierResolver + * @see org.hibernate.boot.SessionFactoryBuilder#applyCurrentTenantIdentifierResolver * * @since 4.1 */ String MULTI_TENANT_IDENTIFIER_RESOLVER = "hibernate.tenant_identifier_resolver"; /** - * During bootstrap, Hibernate needs access to any Connection for access to {@link java.sql.DatabaseMetaData}. - *

- * This setting configures the name of the DataSource to use for this access + * During bootstrap, Hibernate needs access to a {@code Connection} for access + * to the {@link java.sql.DatabaseMetaData}. This setting configures the tenant id + * to use when obtaining the {@link javax.sql.DataSource} to use for this access. */ String TENANT_IDENTIFIER_TO_USE_FOR_ANY_KEY = "hibernate.multi_tenant.datasource.identifier_for_any"; + + /** + * Specifies a {@link org.hibernate.context.spi.TenantSchemaMapper} to use, either: + *

    + *
  • an instance of {@code TenantSchemaMapper}, + *
  • a {@link Class} representing a class that implements {@code TenantSchemaMapper}, or + *
  • the name of a class that implements {@code TenantSchemaMapper}. + *
+ * When a tenant schema mapper is set, {@link java.sql.Connection#setSchema(String)}} + * is called on newly acquired JDBC connections with the schema name returned by + * {@link org.hibernate.context.spi.TenantSchemaMapper#schemaName}. + *

+ * By default, there is no tenant schema mapper. + * + * @see org.hibernate.context.spi.TenantSchemaMapper + * @see org.hibernate.boot.SessionFactoryBuilder#applyTenantSchemaMapper + * + * @since 7.1 + */ + String MULTI_TENANT_SCHEMA_MAPPER = "hibernate.multi_tenant.schema_mapper"; } diff --git a/hibernate-core/src/main/java/org/hibernate/context/spi/AbstractCurrentSessionContext.java b/hibernate-core/src/main/java/org/hibernate/context/spi/AbstractCurrentSessionContext.java index 434d4ff2e5ed..6ff3527c67c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/context/spi/AbstractCurrentSessionContext.java +++ b/hibernate-core/src/main/java/org/hibernate/context/spi/AbstractCurrentSessionContext.java @@ -34,7 +34,7 @@ public SessionFactoryImplementor factory() { protected SessionBuilder baseSessionBuilder() { final SessionBuilderImplementor builder = factory.withOptions(); - final CurrentTenantIdentifierResolver resolver = factory.getCurrentTenantIdentifierResolver(); + final var resolver = factory.getCurrentTenantIdentifierResolver(); if ( resolver != null ) { builder.tenantIdentifier( resolver.resolveCurrentTenantIdentifier() ); } @@ -42,7 +42,7 @@ protected SessionBuilder baseSessionBuilder() { } protected void validateExistingSession(Session existingSession) { - final CurrentTenantIdentifierResolver resolver = factory.getCurrentTenantIdentifierResolver(); + final var resolver = factory.getCurrentTenantIdentifierResolver(); if ( resolver != null && resolver.validateExistingCurrentSessions() ) { final Object currentValue = resolver.resolveCurrentTenantIdentifier(); final JavaType tenantIdentifierJavaType = factory.getTenantIdentifierJavaType(); diff --git a/hibernate-core/src/main/java/org/hibernate/context/spi/CurrentTenantIdentifierResolver.java b/hibernate-core/src/main/java/org/hibernate/context/spi/CurrentTenantIdentifierResolver.java index e4030f757477..45aa1772e12d 100644 --- a/hibernate-core/src/main/java/org/hibernate/context/spi/CurrentTenantIdentifierResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/context/spi/CurrentTenantIdentifierResolver.java @@ -4,6 +4,7 @@ */ package org.hibernate.context.spi; + /** * A callback registered with the {@link org.hibernate.SessionFactory} that is * responsible for resolving the current tenant identifier. diff --git a/hibernate-core/src/main/java/org/hibernate/context/spi/TenantSchemaMapper.java b/hibernate-core/src/main/java/org/hibernate/context/spi/TenantSchemaMapper.java new file mode 100644 index 000000000000..c14f65d07d91 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/context/spi/TenantSchemaMapper.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.context.spi; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.hibernate.Incubating; + +/** + * Obtains the name of a database schema for a given tenant identifier when + * {@linkplain org.hibernate.cfg.MultiTenancySettings#MULTI_TENANT_SCHEMA_MAPPER + * schema-based multitenancy} is enabled. + * + * @param The type of the tenant id + * + * @since 7.1 + * + * @author Gavin King + */ +@Incubating +public interface TenantSchemaMapper { + /** + * The name of the database schema for data belonging to the tenant with the + * given identifier. + *

+ * Called when {@value org.hibernate.cfg.MultiTenancySettings#MULTI_TENANT_SCHEMA_MAPPER} + * is enabled. + * + * @param tenantIdentifier The tenant identifier + * @return The name of the database schema belonging to that tenant + * + * @see org.hibernate.cfg.MultiTenancySettings#MULTI_TENANT_SCHEMA_MAPPER + */ + @NonNull String schemaName(@NonNull T tenantIdentifier); +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/DataSourceBasedMultiTenantConnectionProviderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/DataSourceBasedMultiTenantConnectionProviderImpl.java index 5a5d715bbd72..e9257cee19c9 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/DataSourceBasedMultiTenantConnectionProviderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/DataSourceBasedMultiTenantConnectionProviderImpl.java @@ -87,14 +87,14 @@ public void injectServices(ServiceRegistryImplementor serviceRegistry) { throw new HibernateException( "JNDI name [" + this.jndiName + "] could not be resolved" ); } else if ( namedObject instanceof DataSource datasource ) { - final int loc = this.jndiName.lastIndexOf( '/' ); - baseJndiNamespace = this.jndiName.substring( 0, loc ); - final String prefix = this.jndiName.substring( loc + 1); + final int loc = jndiName.lastIndexOf( '/' ); + baseJndiNamespace = jndiName.substring( 0, loc ); + final String prefix = jndiName.substring( loc + 1); tenantIdentifierForAny = (T) prefix; dataSourceMap().put( tenantIdentifierForAny, datasource ); } else if ( namedObject instanceof Context ) { - baseJndiNamespace = this.jndiName; + baseJndiNamespace = jndiName; final Object configuredTenantId = configurationService.getSettings().get( TENANT_IDENTIFIER_TO_USE_FOR_ANY_KEY ); tenantIdentifierForAny = (T) configuredTenantId; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java index cb57c37c9c0b..c8a4a645df4e 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java @@ -806,5 +806,13 @@ public boolean isActive() { public SqlExceptionHelper getSqlExceptionHelper() { return sqlExceptionHelper; } + + @Override + public void afterObtainConnection(Connection connection) { + } + + @Override + public void beforeReleaseConnection(Connection connection) { + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java index d962cc0c556a..f4587a1e4605 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcCoordinatorImpl.java @@ -76,23 +76,20 @@ public JdbcCoordinatorImpl( Connection userSuppliedConnection, JdbcSessionOwner owner, JdbcServices jdbcServices) { + this.owner = owner; + this.jdbcServices = jdbcServices; this.isUserSuppliedConnection = userSuppliedConnection != null; + this.logicalConnection = createLogicalConnection( userSuppliedConnection, owner ); + } + private static LogicalConnectionImplementor createLogicalConnection( + Connection userSuppliedConnection, + JdbcSessionOwner owner) { final ResourceRegistry resourceRegistry = new ResourceRegistryStandardImpl( owner.getJdbcSessionContext().getEventHandler() ); - if ( isUserSuppliedConnection ) { - this.logicalConnection = new LogicalConnectionProvidedImpl( userSuppliedConnection, resourceRegistry ); - } - else { - this.logicalConnection = new LogicalConnectionManagedImpl( - owner.getJdbcConnectionAccess(), - owner.getJdbcSessionContext(), - owner.getSqlExceptionHelper(), - resourceRegistry - ); - } - this.owner = owner; - this.jdbcServices = jdbcServices; + return userSuppliedConnection == null + ? new LogicalConnectionManagedImpl( owner, resourceRegistry ) + : new LogicalConnectionProvidedImpl( userSuppliedConnection, resourceRegistry ); } private JdbcCoordinatorImpl( @@ -405,7 +402,7 @@ public JdbcResourceTransaction getResourceLocalTransaction() { */ @Override public void serialize(ObjectOutputStream oos) throws IOException { - if ( ! isReadyForSerialization() ) { + if ( !isReadyForSerialization() ) { throw new HibernateException( "Cannot serialize Session while connected" ); } oos.writeBoolean( isUserSuppliedConnection ); @@ -423,21 +420,13 @@ public void serialize(ObjectOutputStream oos) throws IOException { * @throws IOException Trouble accessing the stream * @throws ClassNotFoundException Trouble reading the stream */ - public static JdbcCoordinatorImpl deserialize( - ObjectInputStream ois, - JdbcSessionOwner owner) throws IOException, ClassNotFoundException { + public static JdbcCoordinatorImpl deserialize(ObjectInputStream ois, JdbcSessionOwner owner) + throws IOException, ClassNotFoundException { final boolean isUserSuppliedConnection = ois.readBoolean(); - final LogicalConnectionImplementor logicalConnection; - if ( isUserSuppliedConnection ) { - logicalConnection = LogicalConnectionProvidedImpl.deserialize( ois ); - } - else { - logicalConnection = LogicalConnectionManagedImpl.deserialize( - ois, - owner.getJdbcConnectionAccess(), - owner.getJdbcSessionContext() - ); - } + final var logicalConnection = + isUserSuppliedConnection + ? LogicalConnectionProvidedImpl.deserialize( ois ) + : LogicalConnectionManagedImpl.deserialize( ois, owner ); return new JdbcCoordinatorImpl( logicalConnection, isUserSuppliedConnection, owner ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java index 5b73af08a162..cf6b5bf94bbb 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SessionDelegatorBaseImpl.java @@ -66,6 +66,8 @@ import org.hibernate.stat.SessionStatistics; import org.hibernate.type.format.FormatMapper; +import java.sql.Connection; +import java.sql.SQLException; import java.util.Collection; import java.util.List; import java.util.Map; @@ -1241,4 +1243,14 @@ public Object loadFromSecondLevelCache(EntityPersister persister, EntityKey enti public boolean isIdentifierRollbackEnabled() { return delegate.isIdentifierRollbackEnabled(); } + + @Override + public void afterObtainConnection(Connection connection) throws SQLException { + delegate.afterObtainConnection( connection ); + } + + @Override + public void beforeReleaseConnection(Connection connection) throws SQLException { + delegate.beforeReleaseConnection( connection ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java index 20c61c050270..466beef5b4a5 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/SharedSessionDelegatorBaseImpl.java @@ -43,6 +43,8 @@ import org.hibernate.resource.transaction.spi.TransactionCoordinator; import org.hibernate.type.format.FormatMapper; +import java.sql.Connection; +import java.sql.SQLException; import java.util.List; import java.util.Set; import java.util.TimeZone; @@ -697,4 +699,14 @@ public Object loadFromSecondLevelCache(EntityPersister persister, EntityKey enti public boolean isIdentifierRollbackEnabled() { return delegate.isIdentifierRollbackEnabled(); } + + @Override + public void afterObtainConnection(Connection connection) throws SQLException { + delegate.afterObtainConnection( connection ); + } + + @Override + public void beforeReleaseConnection(Connection connection) throws SQLException { + delegate.beforeReleaseConnection( connection ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java b/hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java index 8efc6cbeb5a8..47ee016b1ba0 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/internal/TenantIdGeneration.java @@ -9,7 +9,6 @@ import org.hibernate.PropertyValueException; import org.hibernate.annotations.TenantId; -import org.hibernate.context.spi.CurrentTenantIdentifierResolver; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.BeforeExecutionGenerator; @@ -51,8 +50,7 @@ public Object generate(SharedSessionContractImplementor session, Object owner, O final SessionFactoryImplementor sessionFactory = session.getSessionFactory(); final Object tenantId = session.getTenantIdentifierValue(); if ( currentValue != null ) { - final CurrentTenantIdentifierResolver resolver = - sessionFactory.getCurrentTenantIdentifierResolver(); + final var resolver = sessionFactory.getCurrentTenantIdentifierResolver(); if ( resolver != null && resolver.isRoot( tenantId ) ) { // the "root" tenant is allowed to set the tenant id explicitly return currentValue; diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index b058c6a77738..009c1974564e 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -27,7 +27,6 @@ import org.hibernate.binder.internal.TenantIdBinder; import org.hibernate.boot.spi.SessionFactoryOptions; import org.hibernate.cache.spi.CacheTransactionSynchronization; -import org.hibernate.context.spi.CurrentTenantIdentifierResolver; import org.hibernate.dialect.Dialect; import org.hibernate.engine.internal.SessionEventListenerManagerImpl; import org.hibernate.engine.jdbc.LobCreator; @@ -107,6 +106,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serial; +import java.sql.Connection; import java.sql.SQLException; import java.util.List; import java.util.Locale; @@ -243,7 +243,7 @@ protected final void setUpMultitenancy(SessionFactoryImplementor factory, LoadQu throw new HibernateException( "SessionFactory configured for multi-tenancy, but no tenant identifier specified" ); } else { - final CurrentTenantIdentifierResolver resolver = factory.getCurrentTenantIdentifierResolver(); + final var resolver = factory.getCurrentTenantIdentifierResolver(); if ( resolver==null || !resolver.isRoot( tenantIdentifier ) ) { // turn on the filter, unless this is the "root" tenant with access to all partitions loadQueryInfluencers.enableFilter( TenantIdBinder.FILTER_NAME ) @@ -676,9 +676,10 @@ public boolean isConnected() { @Override public JdbcConnectionAccess getJdbcConnectionAccess() { - // See class-level JavaDocs for a discussion of the concurrent-access safety of this method + // See class-level Javadoc for a discussion of the concurrent-access safety of this method if ( jdbcConnectionAccess == null ) { if ( !factoryOptions.isMultiTenancyEnabled() ) { + // we might still be using schema-based multitenancy jdbcConnectionAccess = new NonContextualJdbcConnectionAccess( sessionEventsManager, factory.connectionProvider, @@ -686,6 +687,7 @@ public JdbcConnectionAccess getJdbcConnectionAccess() { ); } else { + // we're using datasource-based multitenancy jdbcConnectionAccess = new ContextualJdbcConnectionAccess( tenantIdentifier, sessionEventsManager, @@ -697,6 +699,30 @@ public JdbcConnectionAccess getJdbcConnectionAccess() { return jdbcConnectionAccess; } + private boolean useSchemaBasedMultiTenancy() { + return tenantIdentifier != null + && factory.getSessionFactoryOptions().getTenantSchemaMapper() != null; + } + + private String tenantSchema() { + final var tenantSchemaMapper = factory.getSessionFactoryOptions().getTenantSchemaMapper(); + return tenantSchemaMapper == null ? null : tenantSchemaMapper.schemaName( tenantIdentifier ); + } + + @Override + public void afterObtainConnection(Connection connection) throws SQLException { + if ( useSchemaBasedMultiTenancy() ) { + connection.setSchema( tenantSchema() ); + } + } + + @Override + public void beforeReleaseConnection(Connection connection) throws SQLException { + if ( useSchemaBasedMultiTenancy() ) { + connection.setSchema( null ); + } + } + @Override public EntityKey generateEntityKey(Object id, EntityPersister persister) { return new EntityKey( id, persister ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/ContextualJdbcConnectionAccess.java b/hibernate-core/src/main/java/org/hibernate/internal/ContextualJdbcConnectionAccess.java index 53aead819832..003c99ed45b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/ContextualJdbcConnectionAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/ContextualJdbcConnectionAccess.java @@ -44,17 +44,13 @@ public Connection obtainConnection() throws SQLException { } final EventMonitor eventMonitor = session.getEventMonitor(); - final DiagnosticEvent jdbcConnectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent(); + final DiagnosticEvent connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent(); try { listener.jdbcConnectionAcquisitionStart(); return connectionProvider.getConnection( tenantIdentifier ); } finally { - eventMonitor.completeJdbcConnectionAcquisitionEvent( - jdbcConnectionAcquisitionEvent, - session, - tenantIdentifier - ); + eventMonitor.completeJdbcConnectionAcquisitionEvent( connectionAcquisitionEvent, session, tenantIdentifier ); listener.jdbcConnectionAcquisitionEnd(); } } @@ -66,13 +62,13 @@ public void releaseConnection(Connection connection) throws SQLException { } final EventMonitor eventMonitor = session.getEventMonitor(); - final DiagnosticEvent jdbcConnectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent(); + final DiagnosticEvent connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent(); try { listener.jdbcConnectionReleaseStart(); connectionProvider.releaseConnection( tenantIdentifier, connection ); } finally { - eventMonitor.completeJdbcConnectionReleaseEvent( jdbcConnectionReleaseEvent, session, tenantIdentifier ); + eventMonitor.completeJdbcConnectionReleaseEvent( connectionReleaseEvent, session, tenantIdentifier ); listener.jdbcConnectionReleaseEnd(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java b/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java index 8f91d9e67930..96ae39e771e3 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java @@ -38,17 +38,13 @@ public NonContextualJdbcConnectionAccess( @Override public Connection obtainConnection() throws SQLException { final EventMonitor eventMonitor = session.getEventMonitor(); - final DiagnosticEvent jdbcConnectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent(); + final DiagnosticEvent connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent(); try { listener.jdbcConnectionAcquisitionStart(); return connectionProvider.getConnection(); } finally { - eventMonitor.completeJdbcConnectionAcquisitionEvent( - jdbcConnectionAcquisitionEvent, - session, - null - ); + eventMonitor.completeJdbcConnectionAcquisitionEvent( connectionAcquisitionEvent, session, null ); listener.jdbcConnectionAcquisitionEnd(); } } @@ -56,13 +52,13 @@ public Connection obtainConnection() throws SQLException { @Override public void releaseConnection(Connection connection) throws SQLException { final EventMonitor eventMonitor = session.getEventMonitor(); - final DiagnosticEvent jdbcConnectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent(); + final DiagnosticEvent connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent(); try { listener.jdbcConnectionReleaseStart(); connectionProvider.closeConnection( connection ); } finally { - eventMonitor.completeJdbcConnectionReleaseEvent( jdbcConnectionReleaseEvent, session, null ); + eventMonitor.completeJdbcConnectionReleaseEvent( connectionReleaseEvent, session, null ); listener.jdbcConnectionReleaseEnd(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java index f8200fd0b5d8..86e6ec48ff31 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java @@ -458,7 +458,7 @@ private void disintegrate(Exception startupException, IntegratorObserver integra private SessionBuilderImpl createDefaultSessionOpenOptionsIfPossible() { - final CurrentTenantIdentifierResolver tenantIdResolver = getCurrentTenantIdentifierResolver(); + final var tenantIdResolver = getCurrentTenantIdentifierResolver(); if ( tenantIdResolver == null ) { return withOptions(); } @@ -1154,7 +1154,7 @@ public SessionBuilderImpl(SessionFactoryImpl sessionFactory) { subselectFetchEnabled = sessionFactoryOptions.isSubselectFetchEnabled(); identifierRollback = sessionFactoryOptions.isIdentifierRollbackEnabled(); - final CurrentTenantIdentifierResolver currentTenantIdentifierResolver = + final var currentTenantIdentifierResolver = sessionFactory.getCurrentTenantIdentifierResolver(); if ( currentTenantIdentifierResolver != null ) { tenantIdentifier = currentTenantIdentifierResolver.resolveCurrentTenantIdentifier(); diff --git a/hibernate-core/src/main/java/org/hibernate/relational/SchemaManager.java b/hibernate-core/src/main/java/org/hibernate/relational/SchemaManager.java index cb1d8cb80f3b..6a3a907a6c1e 100644 --- a/hibernate-core/src/main/java/org/hibernate/relational/SchemaManager.java +++ b/hibernate-core/src/main/java/org/hibernate/relational/SchemaManager.java @@ -89,4 +89,23 @@ public interface SchemaManager extends jakarta.persistence.SchemaManager { */ @Incubating void populate(); + + /** + * Obtain an instance which targets the given schema. + * @param schemaName The name of the schema to target + * + * @since 7.1 + */ + @Incubating + SchemaManager forSchema(String schemaName); + + /** + * Obtain an instance which targets the given schema of the given catalog. + * @param schemaName The name of the schema to target + * @param catalogName The name of the catalog to target + * + * @since 7.1 + */ + @Incubating + SchemaManager forSchemaAndCatalog(String schemaName, String catalogName); } diff --git a/hibernate-core/src/main/java/org/hibernate/relational/internal/SchemaManagerImpl.java b/hibernate-core/src/main/java/org/hibernate/relational/internal/SchemaManagerImpl.java index c8b2fbae9c86..a4f1d13474e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/relational/internal/SchemaManagerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/relational/internal/SchemaManagerImpl.java @@ -26,12 +26,33 @@ public class SchemaManagerImpl implements SchemaManager { private final SessionFactoryImplementor sessionFactory; private final MetadataImplementor metadata; + private final String schemaName; + private final String catalogName; public SchemaManagerImpl( SessionFactoryImplementor sessionFactory, MetadataImplementor metadata) { + this( sessionFactory, metadata, null, null ); + } + + public SchemaManagerImpl( + SessionFactoryImplementor sessionFactory, + MetadataImplementor metadata, + String schemaName, String catalogName) { this.sessionFactory = sessionFactory; this.metadata = metadata; + this.schemaName = schemaName; + this.catalogName = catalogName; + } + + @Override + public SchemaManager forSchema(String schemaName) { + return new SchemaManagerImpl( sessionFactory, metadata, schemaName, null ); + } + + @Override + public SchemaManager forSchemaAndCatalog(String schemaName, String catalogName) { + return new SchemaManagerImpl( sessionFactory, metadata, schemaName, catalogName ); } @Override @@ -40,6 +61,8 @@ public void exportMappedObjects(boolean createSchemas) { properties.put( AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, Action.CREATE_ONLY ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_SCRIPTS_ACTION, Action.NONE ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_CREATE_SCHEMAS, createSchemas ); + properties.put( AvailableSettings.DEFAULT_SCHEMA, schemaName ); + properties.put( AvailableSettings.DEFAULT_CATALOG, catalogName ); SchemaManagementToolCoordinator.process( metadata, sessionFactory.getServiceRegistry(), @@ -54,6 +77,8 @@ public void dropMappedObjects(boolean dropSchemas) { properties.put( AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, Action.DROP ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_SCRIPTS_ACTION, Action.NONE ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_CREATE_SCHEMAS, dropSchemas ); + properties.put( AvailableSettings.DEFAULT_SCHEMA, schemaName ); + properties.put( AvailableSettings.DEFAULT_CATALOG, catalogName ); SchemaManagementToolCoordinator.process( metadata, sessionFactory.getServiceRegistry(), @@ -68,6 +93,8 @@ public void validateMappedObjects() { properties.put( AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, Action.VALIDATE ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_SCRIPTS_ACTION, Action.NONE ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_CREATE_SCHEMAS, false ); + properties.put( AvailableSettings.DEFAULT_SCHEMA, schemaName ); + properties.put( AvailableSettings.DEFAULT_CATALOG, catalogName ); SchemaManagementToolCoordinator.process( metadata, sessionFactory.getServiceRegistry(), @@ -81,6 +108,8 @@ public void truncateMappedObjects() { Map properties = new HashMap<>( sessionFactory.getProperties() ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, Action.TRUNCATE ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_SCRIPTS_ACTION, Action.NONE ); + properties.put( AvailableSettings.DEFAULT_SCHEMA, schemaName ); + properties.put( AvailableSettings.DEFAULT_CATALOG, catalogName ); SchemaManagementToolCoordinator.process( metadata, sessionFactory.getServiceRegistry(), @@ -94,6 +123,8 @@ public void populate() { Map properties = new HashMap<>( sessionFactory.getProperties() ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_DATABASE_ACTION, Action.POPULATE ); properties.put( AvailableSettings.JAKARTA_HBM2DDL_SCRIPTS_ACTION, Action.NONE ); + properties.put( AvailableSettings.DEFAULT_SCHEMA, schemaName ); + properties.put( AvailableSettings.DEFAULT_CATALOG, catalogName ); SchemaManagementToolCoordinator.process( metadata, sessionFactory.getServiceRegistry(), diff --git a/hibernate-core/src/main/java/org/hibernate/resource/jdbc/internal/LogicalConnectionManagedImpl.java b/hibernate-core/src/main/java/org/hibernate/resource/jdbc/internal/LogicalConnectionManagedImpl.java index 84b496294af9..aa2b39d81801 100644 --- a/hibernate-core/src/main/java/org/hibernate/resource/jdbc/internal/LogicalConnectionManagedImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/resource/jdbc/internal/LogicalConnectionManagedImpl.java @@ -11,15 +11,12 @@ import java.sql.SQLException; import org.hibernate.ResourceClosedException; -import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; -import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.resource.jdbc.LogicalConnection; import org.hibernate.resource.jdbc.ResourceRegistry; import org.hibernate.resource.jdbc.spi.JdbcEventHandler; -import org.hibernate.resource.jdbc.spi.JdbcSessionContext; +import org.hibernate.resource.jdbc.spi.JdbcSessionOwner; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; import static org.hibernate.ConnectionAcquisitionMode.IMMEDIATELY; @@ -39,91 +36,72 @@ public class LogicalConnectionManagedImpl extends AbstractLogicalConnectionImplementor { private static final CoreMessageLogger log = CoreLogging.messageLogger( LogicalConnectionManagedImpl.class ); - private final transient JdbcConnectionAccess jdbcConnectionAccess; - private final transient JdbcEventHandler jdbcEventHandler; - private final transient SqlExceptionHelper sqlExceptionHelper; - + private final transient JdbcSessionOwner jdbcSessionOwner; private final transient PhysicalConnectionHandlingMode connectionHandlingMode; private transient Connection physicalConnection; private boolean closed; - private final boolean providerDisablesAutoCommit; - - public LogicalConnectionManagedImpl( - JdbcConnectionAccess jdbcConnectionAccess, - JdbcSessionContext jdbcSessionContext, - SqlExceptionHelper sqlExceptionHelper, - ResourceRegistry resourceRegistry) { - this.jdbcConnectionAccess = jdbcConnectionAccess; - this.jdbcEventHandler = jdbcSessionContext.getEventHandler(); + public LogicalConnectionManagedImpl(JdbcSessionOwner sessionOwner, ResourceRegistry resourceRegistry) { + this.jdbcSessionOwner = sessionOwner; this.resourceRegistry = resourceRegistry; - this.connectionHandlingMode = determineConnectionHandlingMode( - jdbcSessionContext.getPhysicalConnectionHandlingMode(), - jdbcConnectionAccess ); - - this.sqlExceptionHelper = sqlExceptionHelper; - + connectionHandlingMode = determineConnectionHandlingMode( sessionOwner ); if ( connectionHandlingMode.getAcquisitionMode() == IMMEDIATELY ) { //noinspection resource acquireConnectionIfNeeded(); } - this.providerDisablesAutoCommit = jdbcSessionContext.doesConnectionProviderDisableAutoCommit(); - if ( providerDisablesAutoCommit ) { + if ( sessionOwner.getJdbcSessionContext().doesConnectionProviderDisableAutoCommit() ) { log.connectionProviderDisablesAutoCommitEnabled(); } } - public LogicalConnectionManagedImpl( - JdbcConnectionAccess jdbcConnectionAccess, - JdbcSessionContext jdbcSessionContext, - ResourceRegistry resourceRegistry, - JdbcServices jdbcServices) { - this( - jdbcConnectionAccess, - jdbcSessionContext, - jdbcServices.getSqlExceptionHelper(), - resourceRegistry - ); - } - - private PhysicalConnectionHandlingMode determineConnectionHandlingMode( - PhysicalConnectionHandlingMode connectionHandlingMode, - JdbcConnectionAccess jdbcConnectionAccess) { + private PhysicalConnectionHandlingMode determineConnectionHandlingMode(JdbcSessionOwner sessionOwner) { + final var connectionHandlingMode = sessionOwner.getJdbcSessionContext().getPhysicalConnectionHandlingMode(); return connectionHandlingMode.getReleaseMode() == AFTER_STATEMENT - && !jdbcConnectionAccess.supportsAggressiveRelease() + && !sessionOwner.getJdbcConnectionAccess().supportsAggressiveRelease() ? DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION : connectionHandlingMode; } - private LogicalConnectionManagedImpl( - JdbcConnectionAccess jdbcConnectionAccess, - JdbcSessionContext jdbcSessionContext, - boolean closed) { - this( - jdbcConnectionAccess, - jdbcSessionContext, - new ResourceRegistryStandardImpl(), - jdbcSessionContext.getJdbcServices() - ); + private LogicalConnectionManagedImpl(JdbcSessionOwner owner, boolean closed) { + this( owner, new ResourceRegistryStandardImpl() ); this.closed = closed; } private Connection acquireConnectionIfNeeded() { if ( physicalConnection == null ) { - jdbcEventHandler.jdbcConnectionAcquisitionStart(); + final JdbcEventHandler eventHandler = jdbcSessionOwner.getJdbcSessionContext().getEventHandler(); + eventHandler.jdbcConnectionAcquisitionStart(); try { - physicalConnection = jdbcConnectionAccess.obtainConnection(); + physicalConnection = jdbcSessionOwner.getJdbcConnectionAccess().obtainConnection(); } catch ( SQLException e ) { - throw sqlExceptionHelper.convert( e, "Unable to acquire JDBC Connection" ); + throw jdbcSessionOwner.getSqlExceptionHelper() + .convert( e, "Unable to acquire JDBC Connection" ); } finally { - jdbcEventHandler.jdbcConnectionAcquisitionEnd( physicalConnection ); + eventHandler.jdbcConnectionAcquisitionEnd( physicalConnection ); } + + try { + jdbcSessionOwner.afterObtainConnection( physicalConnection ); + } + catch (SQLException e) { + try { + // given the session a chance to set the schema + jdbcSessionOwner.getJdbcConnectionAccess().releaseConnection( physicalConnection ); + } + catch (SQLException re) { + e.addSuppressed( re ); + } + throw jdbcSessionOwner.getSqlExceptionHelper() + .convert( e, "Error after acquiring JDBC Connection" ); + } + } + return physicalConnection; } @@ -175,7 +153,7 @@ public void beforeTransactionCompletion() { public void afterTransaction() { super.afterTransaction(); if ( connectionHandlingMode.getReleaseMode() != ON_CLOSE ) { - // NOTE : we check for !ON_CLOSE here (rather than AFTER_TRANSACTION) to also catch: + // NOTE: we check for !ON_CLOSE here (rather than AFTER_TRANSACTION) to also catch: // - AFTER_STATEMENT cases that were circumvented due to held resources // - BEFORE_TRANSACTION_COMPLETION cases that were circumvented because a rollback occurred // (we don't get a beforeTransactionCompletion event on rollback). @@ -205,6 +183,15 @@ public void manualReconnect(Connection suppliedConnection) { private void releaseConnection() { final Connection localVariableConnection = physicalConnection; if ( localVariableConnection != null ) { + try { + // give the session a chance to change the schema back to null + jdbcSessionOwner.beforeReleaseConnection( physicalConnection ); + } + catch (SQLException e) { + log.warn( "Error before releasing JDBC connection", e ); + } + + final JdbcEventHandler eventHandler = jdbcSessionOwner.getJdbcSessionContext().getEventHandler(); // We need to set the connection to null before we release resources, // in order to prevent recursion into this method. // Recursion can happen when we release resources and when batch statements are in progress: @@ -216,19 +203,20 @@ private void releaseConnection() { try { getResourceRegistry().releaseResources(); if ( !localVariableConnection.isClosed() ) { - sqlExceptionHelper.logAndClearWarnings( localVariableConnection ); + jdbcSessionOwner.getSqlExceptionHelper().logAndClearWarnings( localVariableConnection ); } } finally { - jdbcEventHandler.jdbcConnectionReleaseStart(); - jdbcConnectionAccess.releaseConnection( localVariableConnection ); + eventHandler.jdbcConnectionReleaseStart(); + jdbcSessionOwner.getJdbcConnectionAccess().releaseConnection( localVariableConnection ); } } catch (SQLException e) { - throw sqlExceptionHelper.convert( e, "Unable to release JDBC Connection" ); + throw jdbcSessionOwner.getSqlExceptionHelper() + .convert( e, "Unable to release JDBC Connection" ); } finally { - jdbcEventHandler.jdbcConnectionReleaseEnd(); + eventHandler.jdbcConnectionReleaseEnd(); } } } @@ -238,12 +226,9 @@ public void serialize(ObjectOutputStream oos) throws IOException { oos.writeBoolean( closed ); } - public static LogicalConnectionManagedImpl deserialize( - ObjectInputStream ois, - JdbcConnectionAccess jdbcConnectionAccess, - JdbcSessionContext jdbcSessionContext) throws IOException { - final boolean isClosed = ois.readBoolean(); - return new LogicalConnectionManagedImpl( jdbcConnectionAccess, jdbcSessionContext, isClosed ); + public static LogicalConnectionManagedImpl deserialize(ObjectInputStream ois, JdbcSessionOwner owner) + throws IOException { + return new LogicalConnectionManagedImpl( owner, ois.readBoolean() ); } @Override @@ -289,6 +274,6 @@ protected void afterCompletion() { @Override protected boolean doConnectionsFromProviderHaveAutoCommitDisabled() { - return providerDisablesAutoCommit; + return jdbcSessionOwner.getJdbcSessionContext().doesConnectionProviderDisableAutoCommit(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/resource/jdbc/spi/JdbcSessionOwner.java b/hibernate-core/src/main/java/org/hibernate/resource/jdbc/spi/JdbcSessionOwner.java index 4f47a673a526..310433ef825f 100644 --- a/hibernate-core/src/main/java/org/hibernate/resource/jdbc/spi/JdbcSessionOwner.java +++ b/hibernate-core/src/main/java/org/hibernate/resource/jdbc/spi/JdbcSessionOwner.java @@ -4,12 +4,16 @@ */ package org.hibernate.resource.jdbc.spi; +import org.hibernate.Incubating; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.event.spi.EventManager; import org.hibernate.event.monitor.spi.EventMonitor; import org.hibernate.resource.transaction.spi.TransactionCoordinator; +import java.sql.Connection; +import java.sql.SQLException; + import static java.lang.reflect.InvocationHandler.invokeDefault; import static java.lang.reflect.Proxy.newProxyInstance; @@ -78,6 +82,35 @@ default SqlExceptionHelper getSqlExceptionHelper() { return getJdbcSessionContext().getJdbcServices().getSqlExceptionHelper(); } + /** + * Called after a managed JDBC connection is obtained. + *

+ * Sets the schema to the schema belonging to the current tenant if: + *

    + *
  1. {@value org.hibernate.cfg.MultiTenancySettings#MULTI_TENANT_SCHEMA_MAPPER} is enabled, and + *
  2. this session has an active tenant id. + *
+ * + * @param connection A JDBC connection which was just acquired + * + * @since 7.1 + */ + @Incubating + void afterObtainConnection(Connection connection) throws SQLException; + + /** + * Called right before a managed JDBC connection is released. + *

+ * Unset the schema which was set by {@link #afterObtainConnection}, + * if any. + * . + * @param connection The JDBC connection which is about to be released + * + * @since 7.1 + */ + @Incubating + void beforeReleaseConnection(Connection connection) throws SQLException; + /** * Obtain a reference to the {@link EventMonitor}. * diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/spi/SchemaManagementToolCoordinator.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/spi/SchemaManagementToolCoordinator.java index b3b4ceaedbc3..c415099d5188 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/spi/SchemaManagementToolCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/spi/SchemaManagementToolCoordinator.java @@ -120,12 +120,12 @@ public static void process( StandardConverters.BOOLEAN, false ); - final ExceptionHandler exceptionHandler = haltOnError ? ExceptionHandlerHaltImpl.INSTANCE : ExceptionHandlerLoggedImpl.INSTANCE; - final ExecutionOptions executionOptions = buildExecutionOptions( - configurationValues, - exceptionHandler - ); + final ExecutionOptions executionOptions = + buildExecutionOptions( configurationValues, + haltOnError + ? ExceptionHandlerHaltImpl.INSTANCE + : ExceptionHandlerLoggedImpl.INSTANCE ); if ( scriptActionMap != null ) { scriptActionMap.forEach( diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/multitenancy/SchemaBasedMultitenancyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/multitenancy/SchemaBasedMultitenancyTest.java new file mode 100644 index 000000000000..440d993f424c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/multitenancy/SchemaBasedMultitenancyTest.java @@ -0,0 +1,70 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.multitenancy; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.context.spi.TenantSchemaMapper; +import org.hibernate.relational.SchemaManager; +import org.hibernate.annotations.TenantId; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import static org.hibernate.cfg.MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER; +import static org.hibernate.cfg.MultiTenancySettings.MULTI_TENANT_SCHEMA_MAPPER; + +@Jpa(annotatedClasses = {SchemaBasedMultitenancyTest.Person.class}, + integrationSettings = + {@Setting(name = MULTI_TENANT_SCHEMA_MAPPER, + value = "org.hibernate.orm.test.multitenancy.SchemaBasedMultitenancyTest$MyMapper"), + @Setting(name = MULTI_TENANT_IDENTIFIER_RESOLVER, + value = "org.hibernate.orm.test.multitenancy.SchemaBasedMultitenancyTest$MyResolver")}) +public class SchemaBasedMultitenancyTest { + @Test void test(EntityManagerFactoryScope scope) { + var schemaManager = (SchemaManager) scope.getEntityManagerFactory().getSchemaManager(); + SchemaManager managerForTenantSchema = schemaManager.forSchema( "gavin" ); + managerForTenantSchema.drop(true); + managerForTenantSchema.create( true ); + scope.inTransaction( session -> { + Person person = new Person(); + person.ssn = "123456789"; + person.tenantId = "gavin"; + person.name = "Gavin"; + session.persist( person ); + } ); + } + + @Entity static class Person { + @Id + String ssn; + @TenantId + String tenantId; + private String name; + } + + public static class MyResolver implements CurrentTenantIdentifierResolver { + @Override + public @NonNull String resolveCurrentTenantIdentifier() { + return "gavin"; + } + + @Override + public boolean validateExistingCurrentSessions() { + return false; + } + } + + public static class MyMapper implements TenantSchemaMapper { + @Override + public @NonNull String schemaName(@NonNull String tenantIdentifier) { + return tenantIdentifier.toUpperCase(); + } + } + +} diff --git a/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventMonitor.java b/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventMonitor.java index 5775c1e7f844..a2c9af2b9111 100644 --- a/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventMonitor.java +++ b/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventMonitor.java @@ -780,10 +780,7 @@ public void completeCollectionRemoveEvent( } private String getSessionIdentifier(SharedSessionContractImplementor session) { - if ( session == null ) { - return null; - } - return session.getSessionIdentifier().toString(); + return session == null ? null : session.getSessionIdentifier().toString(); } private String getEntityName(EntityPersister persister) {