diff --git a/hibernate-core/src/main/java/org/hibernate/Session.java b/hibernate-core/src/main/java/org/hibernate/Session.java index 45920d944eff..5daa9e7254e1 100644 --- a/hibernate-core/src/main/java/org/hibernate/Session.java +++ b/hibernate-core/src/main/java/org/hibernate/Session.java @@ -444,6 +444,8 @@ public interface Session extends SharedSessionContract, EntityManager { * * @param readOnly {@code true}, the default for loaded entities/proxies is read-only; * {@code false}, the default for loaded entities/proxies is modifiable + * @throws SessionException if the session was originally + * {@linkplain SessionBuilder#readOnly created in read-only mode} */ void setDefaultReadOnly(boolean readOnly); diff --git a/hibernate-core/src/main/java/org/hibernate/SessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/SessionBuilder.java index 0410837eae44..a064ede186bc 100644 --- a/hibernate-core/src/main/java/org/hibernate/SessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/SessionBuilder.java @@ -165,6 +165,60 @@ public interface SessionBuilder { */ SessionBuilder tenantIdentifier(Object tenantIdentifier); + /** + * Specify a {@linkplain Session#isDefaultReadOnly read-only mode} + * for the session. If a session is created in read-only mode, then + * {@link Connection#setReadOnly} is called when a JDBC connection + * is obtained. + *

+ * Furthermore, if read/write replication is in use, then: + *

+ *

+ * When read/write replication is in use, it's strongly recommended + * that the session be created with the {@linkplain #initialCacheMode + * initial cache mode} set to {@link CacheMode#GET}, to avoid writing + * stale data read from a read-only replica to the second-level cache. + * Hibernate cannot possibly guarantee that data read from a read-only + * replica is up to date. + *

+ * When read/write replication is in use, it's possible that an item + * read from the second-level cache might refer to data which does not + * yet exist in the read-only replica. In this situation, an exception + * occurs when the association is fetched. To completely avoid this + * possibility, the {@linkplain #initialCacheMode initial cache mode} + * must be set to {@link CacheMode#IGNORE}. However, it's also usually + * possible to structure data access code in a way which eliminates + * this possibility. + *

+ * If a session is created in read-only mode, then it cannot be + * changed to read-write mode, and any call to + * {@link Session#setDefaultReadOnly(boolean)} with fail. On the + * other hand, if a session is created in read-write mode, then it + * may later be switched to read-only mode, but all database access + * is directed to the writable replica. + * + * @return {@code this}, for method chaining + * @since 7.2 + * + * @see org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider#getReadOnlyConnection(Object) + * @see org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider#releaseReadOnlyConnection(Object, Connection) + */ + @Incubating + SessionBuilder readOnly(boolean readOnly); + + /** + * Specify the initial {@link CacheMode} for the session. + * + * @return {@code this}, for method chaining + * @since 7.2 + * + * @see SharedSessionContract#getCacheMode() + */ + SessionBuilder initialCacheMode(CacheMode cacheMode); + /** * Add one or more {@link SessionEventListener} instances to the list of * listeners for the new session to be built. diff --git a/hibernate-core/src/main/java/org/hibernate/SharedSessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/SharedSessionBuilder.java index 77c3b863ddcb..ad4374c1363e 100644 --- a/hibernate-core/src/main/java/org/hibernate/SharedSessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/SharedSessionBuilder.java @@ -96,6 +96,12 @@ public interface SharedSessionBuilder extends SessionBuilder { @Override SharedSessionBuilder tenantIdentifier(Object tenantIdentifier); + @Override + SharedSessionBuilder readOnly(boolean readOnly); + + @Override + SharedSessionBuilder initialCacheMode(CacheMode cacheMode); + @Override SharedSessionBuilder eventListeners(SessionEventListener... listeners); diff --git a/hibernate-core/src/main/java/org/hibernate/StatelessSessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/StatelessSessionBuilder.java index e58c735f3651..bac5a680f04f 100644 --- a/hibernate-core/src/main/java/org/hibernate/StatelessSessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/StatelessSessionBuilder.java @@ -41,7 +41,7 @@ public interface StatelessSessionBuilder { * @return {@code this}, for method chaining * @deprecated Use {@link #tenantIdentifier(Object)} instead */ - @Deprecated(forRemoval = true) + @Deprecated(since = "6.4", forRemoval = true) StatelessSessionBuilder tenantIdentifier(String tenantIdentifier); /** @@ -54,13 +54,59 @@ public interface StatelessSessionBuilder { */ StatelessSessionBuilder tenantIdentifier(Object tenantIdentifier); + /** + * Specify a read-only mode for the stateless session. If a session + * is created in read-only mode, then {@link Connection#setReadOnly} + * is called when a JDBC connection is obtained. + *

+ * Furthermore, if read/write replication is in use, then: + *

+ *

+ * When read/write replication is in use, it's strongly recommended + * that the session be created with the {@linkplain #initialCacheMode + * initial cache mode} set to {@link CacheMode#GET}, to avoid writing + * stale data read from a read-only replica to the second-level cache. + * Hibernate cannot possibly guarantee that data read from a read-only + * replica is up to date. It's also possible for a read-only session to + *

+ * When read/write replication is in use, it's possible that an item + * read from the second-level cache might refer to data which does not + * yet exist in the read-only replica. In this situation, an exception + * occurs when the association is fetched. To completely avoid this + * possibility, the {@linkplain #initialCacheMode initial cache mode} + * must be set to {@link CacheMode#IGNORE}. However, it's also usually + * possible to structure data access code in a way which eliminates + * this possibility. + * + * @return {@code this}, for method chaining + * @since 7.2 + * + * @see org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider#getReadOnlyConnection(Object) + * @see org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider#releaseReadOnlyConnection(Object, Connection) + */ + @Incubating + StatelessSessionBuilder readOnly(boolean readOnly); + + /** + * Specify the initial {@link CacheMode} for the session. + * + * @return {@code this}, for method chaining + * @since 7.2 + * + * @see SharedSessionContract#getCacheMode() + */ + StatelessSessionBuilder initialCacheMode(CacheMode cacheMode); + /** * Applies the given statement inspection function to the session. * * @param operator An operator which accepts a SQL string, returning * a processed SQL string to be used by Hibernate - * instead of the given original SQL. Alternatively. - * the operator may work by side effect, and simply + * instead of the given original SQL. Alternatively, + * the operator may work by side effect and simply * return the original SQL. * * @return {@code this}, for method chaining diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/MetadataBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/MetadataBuilderImpl.java index 37df701b6c1f..e41ea25d2783 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/MetadataBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/MetadataBuilderImpl.java @@ -10,6 +10,7 @@ import org.hibernate.AnnotationException; import org.hibernate.HibernateException; +import org.hibernate.context.spi.MultiTenancy; import org.hibernate.type.TimeZoneStorageStrategy; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.TimeZoneStorageType; @@ -65,7 +66,6 @@ import org.hibernate.dialect.Dialect; import org.hibernate.dialect.TimeZoneSupport; import org.hibernate.engine.config.spi.ConfigurationService; -import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; @@ -651,7 +651,7 @@ public MetadataBuildingOptionsImpl(StandardServiceRegistry serviceRegistry) { defaultTimezoneStorage = resolveTimeZoneStorageStrategy( configService ); wrapperArrayHandling = resolveWrapperArrayHandling( configService ); - multiTenancyEnabled = JdbcEnvironmentImpl.isMultiTenancyEnabled( serviceRegistry ); + multiTenancyEnabled = MultiTenancy.isMultiTenancyEnabled( serviceRegistry ); xmlMappingEnabled = configService.getSetting( AvailableSettings.XML_MAPPING_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 5bfc1a41e07a..addd2788ceda 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.MultiTenancy; import org.hibernate.context.spi.TenantSchemaMapper; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; @@ -49,7 +50,6 @@ import org.hibernate.context.spi.CurrentTenantIdentifierResolver; import org.hibernate.dialect.Dialect; import org.hibernate.engine.config.spi.ConfigurationService; -import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl; import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.id.uuid.LocalObjectUuidHelper; @@ -74,7 +74,6 @@ import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.query.sqm.sql.SqmTranslatorFactory; -import org.hibernate.resource.beans.internal.Helper; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; import org.hibernate.resource.jdbc.spi.StatementInspector; import org.hibernate.resource.transaction.spi.TransactionCoordinatorBuilder; @@ -347,22 +346,9 @@ public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, Boo initializeLazyStateOutsideTransactions = configurationService.getSetting( ENABLE_LAZY_LOAD_NO_TRANS, BOOLEAN, false ); - multiTenancyEnabled = JdbcEnvironmentImpl.isMultiTenancyEnabled( serviceRegistry ); - currentTenantIdentifierResolver = - strategySelector.resolveStrategy( CurrentTenantIdentifierResolver.class, - settings.get( MULTI_TENANT_IDENTIFIER_RESOLVER ) ); - if ( currentTenantIdentifierResolver == null ) { - currentTenantIdentifierResolver = Helper.getBean( - Helper.getBeanContainer( serviceRegistry ), - CurrentTenantIdentifierResolver.class, - true, - false, - null - ); - } - tenantSchemaMapper = - strategySelector.resolveStrategy( TenantSchemaMapper.class, - settings.get( MULTI_TENANT_SCHEMA_MAPPER ) ); + multiTenancyEnabled = MultiTenancy.isMultiTenancyEnabled( serviceRegistry ); + currentTenantIdentifierResolver = MultiTenancy.getTenantIdentifierResolver( settings, serviceRegistry ); + tenantSchemaMapper = MultiTenancy.getTenantSchemaMapper( settings, serviceRegistry ); delayBatchFetchLoaderCreations = configurationService.getSetting( DELAY_ENTITY_LOADER_CREATIONS, BOOLEAN, true ); @@ -416,7 +402,7 @@ public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, Boo preferredSqlTypeCodeForArray = ConfigurationHelper.getPreferredSqlTypeCodeForArray( serviceRegistry ); defaultTimeZoneStorageStrategy = context.getMetadataBuildingOptions().getDefaultTimeZoneStorage(); - final RegionFactory regionFactory = serviceRegistry.getService( RegionFactory.class ); + final var regionFactory = serviceRegistry.getService( RegionFactory.class ); if ( !(regionFactory instanceof NoCachingRegionFactory) ) { secondLevelCacheEnabled = configurationService.getSetting( USE_SECOND_LEVEL_CACHE, BOOLEAN, true ); diff --git a/hibernate-core/src/main/java/org/hibernate/context/spi/MultiTenancy.java b/hibernate-core/src/main/java/org/hibernate/context/spi/MultiTenancy.java new file mode 100644 index 000000000000..6375d367d71f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/context/spi/MultiTenancy.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.context.spi; + +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.Incubating; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.selector.spi.StrategySelector; +import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; +import org.hibernate.resource.beans.internal.Helper; +import org.hibernate.service.ServiceRegistry; + +import java.util.Map; + +import static org.hibernate.cfg.MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER; +import static org.hibernate.cfg.MultiTenancySettings.MULTI_TENANT_SCHEMA_MAPPER; + +/** + * Exposes useful multitenancy-related strategy objects to user-written components. + *

+ * The operation {@link #getTenantSchemaMapper} is especially useful in any custom + * implementation of {@link MultiTenantConnectionProvider} which takes on responsibility + * for {@linkplain MultiTenantConnectionProvider#handlesConnectionSchema setting the schema}. + * + * @since 7.1 + * + * @author Gavin King + */ +@Incubating +public class MultiTenancy { + + /** + * Is a {@link MultiTenantConnectionProvider} available? + */ + public static boolean isMultiTenancyEnabled(ServiceRegistry serviceRegistry) { + return serviceRegistry.getService( MultiTenantConnectionProvider.class ) != null; + } + + /** + * Obtain the configured {@link CurrentTenantIdentifierResolver}. + */ + @SuppressWarnings("unchecked") + @Nullable + public static CurrentTenantIdentifierResolver getTenantIdentifierResolver( + Map settings, StandardServiceRegistry registry) { + final var currentTenantIdentifierResolver = + registry.requireService( StrategySelector.class ) + .resolveStrategy( CurrentTenantIdentifierResolver.class, + settings.get( MULTI_TENANT_IDENTIFIER_RESOLVER ) ); + if ( currentTenantIdentifierResolver == null ) { + return Helper.getBean( + Helper.getBeanContainer( registry ), + CurrentTenantIdentifierResolver.class, + true, + false, + null + ); + } + else { + return currentTenantIdentifierResolver; + } + } + + /** + * Obtain the configured {@link TenantSchemaMapper}. + */ + @SuppressWarnings("unchecked") + @Nullable + public static TenantSchemaMapper getTenantSchemaMapper( + Map settings, StandardServiceRegistry registry) { + final var tenantSchemaMapper = + registry.requireService( StrategySelector.class ) + .resolveStrategy( TenantSchemaMapper.class, + settings.get( MULTI_TENANT_SCHEMA_MAPPER ) ); + if ( tenantSchemaMapper == null ) { + return Helper.getBean( + Helper.getBeanContainer( registry ), + TenantSchemaMapper.class, + true, + false, + null + ); + } + return tenantSchemaMapper; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderInitiator.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderInitiator.java index 5c355ed2b989..4f678b7bf0b1 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderInitiator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/ConnectionProviderInitiator.java @@ -42,7 +42,7 @@ import static org.hibernate.cfg.JdbcSettings.URL; import static org.hibernate.cfg.JdbcSettings.USER; import static org.hibernate.cfg.SchemaToolingSettings.ENABLE_SYNONYMS; -import static org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl.isMultiTenancyEnabled; +import static org.hibernate.context.spi.MultiTenancy.isMultiTenancyEnabled; import static org.hibernate.internal.util.StringHelper.isBlank; import static org.hibernate.internal.util.StringHelper.nullIfBlank; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/ConnectionProvider.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/ConnectionProvider.java index b6efa7851df8..14e7206285e3 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/ConnectionProvider.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/ConnectionProvider.java @@ -7,6 +7,7 @@ import java.sql.Connection; import java.sql.SQLException; +import org.hibernate.Incubating; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.connections.internal.DatabaseConnectionInfoImpl; import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData; @@ -40,20 +41,62 @@ public interface ConnectionProvider extends Service, Wrapped { * @return The obtained JDBC connection * * @throws SQLException Indicates a problem opening a connection - * @throws org.hibernate.HibernateException Indicates a problem otherwise obtaining a connection. + * @throws org.hibernate.HibernateException Indicates a problem obtaining a connection. */ Connection getConnection() throws SQLException; + /** + * Obtains a connection to a read-only replica for use according to the underlying + * strategy of this provider. + * + * @return The obtained JDBC connection + * + * @throws SQLException Indicates a problem opening a connection + * @throws org.hibernate.HibernateException Indicates a problem obtaining a connection. + * + * @implNote This default implementation simply calls {@link #getConnection()}, + * which returns a connection to a writable replica. If this operation is overridden + * to return a connection to a distinct read-only replica, the matching operation + * {@link #closeReadOnlyConnection(Connection)} must also be overridden. + * + * @since 7.2 + */ + @Incubating + default Connection getReadOnlyConnection() throws SQLException { + return getConnection(); + } + /** * Release a connection from Hibernate use. * * @param connection The JDBC connection to release * * @throws SQLException Indicates a problem closing the connection - * @throws org.hibernate.HibernateException Indicates a problem otherwise releasing a connection. + * @throws org.hibernate.HibernateException Indicates a problem releasing a connection. */ void closeConnection(Connection connection) throws SQLException; + /** + * Release a connection to a read-only replica from Hibernate use. + * + * @param connection The JDBC connection to release + * + * @throws SQLException Indicates a problem closing the connection + * @throws org.hibernate.HibernateException Indicates a problem otherwise releasing a connection. + * + * @implNote This default implementation simply calls + * {@link #closeConnection(Connection)}. If + * {@link #getReadOnlyConnection()} is overridden to return a + * connection to a distinct read-only replica, this operation must also + * be overridden. + * + * @since 7.2 + */ + @Incubating + default void closeReadOnlyConnection(Connection connection) throws SQLException { + closeConnection( connection ); + } + /** * Does this connection provider support aggressive release of JDBC connections and later * re-acquisition of those connections if needed? @@ -72,6 +115,34 @@ public interface ConnectionProvider extends Service, Wrapped { */ boolean supportsAggressiveRelease(); + /** + * Does this connection provider correctly set the + * {@linkplain java.sql.Connection#setSchema schema} + * of the returned JDBC connections? + * @return {@code true} if the connection provider handles this; + * {@code false} if the client should set the schema + * + * @implNote If necessary, a {@code ConnectionProvider} may + * call {@link org.hibernate.context.spi.MultiTenancy#getTenantSchemaMapper} + * to obtain the {@link org.hibernate.context.spi.TenantSchemaMapper}. + */ + @Incubating + default boolean handlesConnectionSchema() { + return false; + } + + /** + * Does this connection provider correctly set the + * {@linkplain java.sql.Connection#setReadOnly read-only mode} + * of the returned JDBC connections? + * @return {@code true} if the connection provider handles this; + * {@code false} if the client should set the read-only mode + */ + @Incubating + default boolean handlesConnectionReadOnly() { + return false; + } + /** * @return an informative instance of {@link DatabaseConnectionInfo} for logging. * diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/MultiTenantConnectionProvider.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/MultiTenantConnectionProvider.java index f3a5dad9df8e..e36b5a4dc2fb 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/MultiTenantConnectionProvider.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/spi/MultiTenantConnectionProvider.java @@ -7,6 +7,7 @@ import java.sql.Connection; import java.sql.SQLException; +import org.hibernate.Incubating; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.connections.internal.DatabaseConnectionInfoImpl; import org.hibernate.service.Service; @@ -21,6 +22,9 @@ *

* An application usually implements its own custom {@code MultiTenantConnectionProvider} * by subclassing {@link AbstractMultiTenantConnectionProvider}. + *

+ * Support for read-only replicas may be implemented by overriding the operations + * {@link #getReadOnlyConnection} and {@link #releaseReadOnlyConnection}. * * @param The tenant identifier type * @@ -57,10 +61,34 @@ public interface MultiTenantConnectionProvider extends Service, Wrapped { * @return The obtained JDBC connection * * @throws SQLException Indicates a problem opening a connection - * @throws org.hibernate.HibernateException Indicates a problem otherwise obtaining a connection. + * @throws org.hibernate.HibernateException Indicates a problem obtaining a connection */ Connection getConnection(T tenantIdentifier) throws SQLException; + /** + * Obtains a connection to a read-only replica for use according to the underlying + * strategy of this provider. + * + * @param tenantIdentifier The identifier of the tenant for which to get a connection + * + * @return The obtained JDBC connection + * + * @throws SQLException Indicates a problem opening a connection + * @throws org.hibernate.HibernateException Indicates a problem obtaining a connection + * + * @implNote This default implementation simply calls {@link #getConnection(Object)}, + * which returns a connection to a writable replica. If this operation is overridden + * to return a connection to a distinct read-only replica, the matching operation + * {@link #releaseReadOnlyConnection(Object, Connection)} must also be overridden. + * + * @since 7.2 + */ + @Incubating + default Connection getReadOnlyConnection(T tenantIdentifier) + throws SQLException { + return getConnection( tenantIdentifier ); + } + /** * Release a connection from Hibernate use. * @@ -68,10 +96,33 @@ public interface MultiTenantConnectionProvider extends Service, Wrapped { * @param tenantIdentifier The identifier of the tenant. * * @throws SQLException Indicates a problem closing the connection - * @throws org.hibernate.HibernateException Indicates a problem otherwise releasing a connection. + * @throws org.hibernate.HibernateException Indicates a problem releasing a connection */ void releaseConnection(T tenantIdentifier, Connection connection) throws SQLException; + /** + * Release a connection to a read-only replica from Hibernate use. + * + * @param connection The JDBC connection to release + * @param tenantIdentifier The identifier of the tenant. + * + * @throws SQLException Indicates a problem closing the connection + * @throws org.hibernate.HibernateException Indicates a problem releasing a connection + * + * @implNote This default implementation simply calls + * {@link #releaseConnection(Object, Connection)}. If + * {@link #getReadOnlyConnection(Object)} is overridden to return a + * connection to a distinct read-only replica, this operation must also + * be overridden. + * + * @since 7.2 + */ + @Incubating + default void releaseReadOnlyConnection(T tenantIdentifier, Connection connection) + throws SQLException { + releaseConnection( tenantIdentifier, connection ); + } + /** * Does this connection provider support aggressive release of JDBC connections and later * re-acquisition of those connections if needed? @@ -90,6 +141,34 @@ public interface MultiTenantConnectionProvider extends Service, Wrapped { */ boolean supportsAggressiveRelease(); + /** + * Does this connection provider correctly set the + * {@linkplain java.sql.Connection#setSchema schema} + * of the returned JDBC connections? + * @return {@code true} if the connection provider handles this; + * {@code false} if the client should set the schema + * + * @implNote If necessary, a {@code ConnectionProvider} may + * call {@link org.hibernate.context.spi.MultiTenancy#getTenantSchemaMapper} + * to obtain the {@link org.hibernate.context.spi.TenantSchemaMapper}. + */ + @Incubating + default boolean handlesConnectionSchema() { + return false; + } + + /** + * Does this connection provider correctly set the + * {@linkplain java.sql.Connection#setReadOnly read-only mode} + * of the returned JDBC connections? + * @return {@code true} if the connection provider handles this; + * {@code false} if the client should set the read-only mode + */ + @Incubating + default boolean handlesConnectionReadOnly() { + return false; + } + default DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { return new DatabaseConnectionInfoImpl( dialect ); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentImpl.java index 98f2d3877b4f..7a56ee26a06c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentImpl.java @@ -14,7 +14,6 @@ import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.StandardConverters; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; -import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; import org.hibernate.engine.jdbc.env.spi.ExtractedDatabaseMetaData; import org.hibernate.engine.jdbc.env.spi.IdentifierHelper; import org.hibernate.engine.jdbc.env.spi.IdentifierHelperBuilder; @@ -27,7 +26,6 @@ import org.hibernate.exception.internal.SQLStateConversionDelegate; import org.hibernate.exception.internal.StandardSQLExceptionConverter; import org.hibernate.exception.spi.SQLExceptionConversionDelegate; -import org.hibernate.service.ServiceRegistry; import org.hibernate.service.spi.ServiceRegistryImplementor; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; @@ -46,10 +44,6 @@ public class JdbcEnvironmentImpl implements JdbcEnvironment { private static final Logger log = Logger.getLogger( JdbcEnvironmentImpl.class ); - public static boolean isMultiTenancyEnabled(ServiceRegistry serviceRegistry) { - return serviceRegistry.getService( MultiTenantConnectionProvider.class ) != null; - } - private final Dialect dialect; private final SqlAstTranslatorFactory sqlAstTranslatorFactory; 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 1ae94ee08772..34bd963d416b 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 @@ -67,7 +67,7 @@ import static org.hibernate.cfg.JdbcSettings.DIALECT_DB_VERSION; import static org.hibernate.cfg.JdbcSettings.JAKARTA_HBM2DDL_DB_VERSION; import static org.hibernate.engine.config.spi.StandardConverters.BOOLEAN; -import static org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl.isMultiTenancyEnabled; +import static org.hibernate.context.spi.MultiTenancy.isMultiTenancyEnabled; import static org.hibernate.internal.log.DeprecationLogger.DEPRECATION_LOGGER; import static org.hibernate.internal.util.NullnessHelper.coalesceSuppliedValues; import static org.hibernate.internal.util.StringHelper.isNotEmpty; @@ -185,14 +185,12 @@ protected void logConnectionInfo(DatabaseConnectionInfo databaseConnectionInfo) } private DatabaseConnectionInfo buildInfo(ServiceRegistryImplementor registry, JdbcEnvironment environment) { - if ( isMultiTenancyEnabled( registry ) ) { - return registry.requireService( MultiTenantConnectionProvider.class ) - .getDatabaseConnectionInfo( environment.getDialect() ); - } - else { - return registry.requireService( ConnectionProvider.class ) - .getDatabaseConnectionInfo( environment.getDialect(), environment.getExtractedDatabaseMetaData() ); - } + return isMultiTenancyEnabled( registry ) + ? registry.requireService( MultiTenantConnectionProvider.class ) + .getDatabaseConnectionInfo( environment.getDialect() ) + : registry.requireService( ConnectionProvider.class ) + .getDatabaseConnectionInfo( environment.getDialect(), + environment.getExtractedDatabaseMetaData() ); } private DatabaseConnectionInfo buildInfo(Map configurationValues, JdbcEnvironment environment) { @@ -453,32 +451,21 @@ private static void logDatabaseAndDriver(DatabaseMetaData dbmd) throws SQLExcept private static boolean explicitDialectConfiguration(String explicitDatabaseName, Map configurationValues) { - return isNotEmpty( explicitDatabaseName ) || isNotNullAndNotEmpty( configurationValues.get( DIALECT ) ); + return isNotEmpty( explicitDatabaseName ) + || isNotNullAndNotEmpty( configurationValues.get( DIALECT ) ); } private static boolean isNotNullAndNotEmpty(Object object) { return object != null - && ( !(object instanceof String string) || !string.isEmpty() ); - } - - private JdbcConnectionAccess buildJdbcConnectionAccess(ServiceRegistryImplementor registry) { - if ( !isMultiTenancyEnabled( registry ) ) { - return new ConnectionProviderJdbcConnectionAccess( registry.requireService( ConnectionProvider.class ) ); - } - else { - final MultiTenantConnectionProvider multiTenantConnectionProvider = - registry.getService( MultiTenantConnectionProvider.class ); - return new MultiTenantConnectionProviderJdbcConnectionAccess( multiTenantConnectionProvider ); - } + && !( object instanceof String string && string.isEmpty() ); } - public static JdbcConnectionAccess buildBootstrapJdbcConnectionAccess(ServiceRegistryImplementor registry) { + public static JdbcConnectionAccess buildJdbcConnectionAccess(ServiceRegistryImplementor registry) { if ( !isMultiTenancyEnabled( registry ) ) { return new ConnectionProviderJdbcConnectionAccess( registry.requireService( ConnectionProvider.class ) ); } else { - final MultiTenantConnectionProvider multiTenantConnectionProvider = - registry.getService( MultiTenantConnectionProvider.class ); + final var multiTenantConnectionProvider = registry.getService( MultiTenantConnectionProvider.class ); return new MultiTenantConnectionProviderJdbcConnectionAccess( multiTenantConnectionProvider ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcServicesImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcServicesImpl.java index 380ed95aadcd..cdf495f00b72 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcServicesImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/internal/JdbcServicesImpl.java @@ -59,7 +59,7 @@ public JdbcEnvironment getJdbcEnvironment() { @Override public JdbcConnectionAccess getBootstrapJdbcConnectionAccess() { - return JdbcEnvironmentInitiator.buildBootstrapJdbcConnectionAccess( serviceRegistry ); + return JdbcEnvironmentInitiator.buildJdbcConnectionAccess( serviceRegistry ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSessionBuilder.java index 77bbfc5114bf..6fe91e7a55e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSessionBuilder.java @@ -8,6 +8,7 @@ import java.util.TimeZone; import java.util.function.UnaryOperator; +import org.hibernate.CacheMode; import org.hibernate.ConnectionAcquisitionMode; import org.hibernate.ConnectionReleaseMode; import org.hibernate.FlushMode; @@ -100,6 +101,18 @@ public SessionBuilder tenantIdentifier(Object tenantIdentifier) { return this; } + @Override + public SessionBuilder readOnly(boolean readOnly) { + delegate.readOnly( readOnly ); + return this; + } + + @Override + public SessionBuilder initialCacheMode(CacheMode cacheMode) { + delegate.initialCacheMode( cacheMode ); + return this; + } + @Override public SessionBuilder eventListeners(SessionEventListener... listeners) { delegate.eventListeners( listeners ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSharedSessionBuilder.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSharedSessionBuilder.java index a37e0b4d4e66..1a6de07f196c 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSharedSessionBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/AbstractDelegatingSharedSessionBuilder.java @@ -8,6 +8,7 @@ import java.util.TimeZone; import java.util.function.UnaryOperator; +import org.hibernate.CacheMode; import org.hibernate.ConnectionAcquisitionMode; import org.hibernate.ConnectionReleaseMode; import org.hibernate.FlushMode; @@ -137,6 +138,18 @@ public SharedSessionBuilder tenantIdentifier(Object tenantIdentifier) { return this; } + @Override + public SharedSessionBuilder readOnly(boolean readOnly) { + delegate.readOnly( readOnly ); + return this; + } + + @Override + public SharedSessionBuilder initialCacheMode(CacheMode cacheMode) { + delegate.initialCacheMode( cacheMode ); + return this; + } + @Override public SharedSessionBuilder eventListeners(SessionEventListener... listeners) { delegate.eventListeners( listeners ); 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 87445914da21..0b2f2bccaede 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -155,6 +155,7 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont private final Interceptor interceptor; private final Object tenantIdentifier; + private final boolean readOnly; private final TimeZone jdbcTimeZone; // mutable state @@ -179,22 +180,25 @@ public abstract class AbstractSharedSessionContract implements SharedSessionCont public AbstractSharedSessionContract(SessionFactoryImpl factory, SessionCreationOptions options) { this.factory = factory; - this.factoryOptions = factory.getSessionFactoryOptions(); - this.jdbcServices = factory.getJdbcServices(); + factoryOptions = factory.getSessionFactoryOptions(); + jdbcServices = factory.getJdbcServices(); cacheTransactionSynchronization = factory.getCache().getRegionFactory().createTransactionContext( this ); + tenantIdentifier = getTenantId( factoryOptions, options ); + readOnly = options.isReadOnly(); + cacheMode = options.getInitialCacheMode(); interceptor = interpret( options.getInterceptor() ); jdbcTimeZone = options.getJdbcTimeZone(); + sessionEventsManager = createSessionEventsManager( factoryOptions, options ); entityNameResolver = new CoordinatingEntityNameResolver( factory, interceptor ); - setCriteriaCopyTreeEnabled( factoryOptions.isCriteriaCopyTreeEnabled() ); - setCriteriaPlanCacheEnabled( factoryOptions.isCriteriaPlanCacheEnabled() ); - setNativeJdbcParametersIgnored( factoryOptions.getNativeJdbcParametersIgnored() ); - setCacheMode( factoryOptions.getInitialSessionCacheMode() ); + criteriaCopyTreeEnabled = factoryOptions.isCriteriaCopyTreeEnabled(); + criteriaPlanCacheEnabled = factoryOptions.isCriteriaPlanCacheEnabled(); + nativeJdbcParametersIgnored = factoryOptions.getNativeJdbcParametersIgnored(); - final StatementInspector statementInspector = interpret( options.getStatementInspector() ); + final var statementInspector = interpret( options.getStatementInspector() ); isTransactionCoordinatorShared = isTransactionCoordinatorShared( options ); if ( isTransactionCoordinatorShared ) { @@ -290,6 +294,10 @@ private static Object getTenantId( SessionFactoryOptions factoryOptions, Session return tenantIdentifier; } + boolean isReadOnly() { + return readOnly; + } + private static SessionEventListenerManager createSessionEventsManager( SessionFactoryOptions factoryOptions, SessionCreationOptions options) { final var customListeners = options.getCustomSessionEventListener(); @@ -679,6 +687,7 @@ public JdbcConnectionAccess getJdbcConnectionAccess() { if ( !factoryOptions.isMultiTenancyEnabled() ) { // we might still be using schema-based multitenancy jdbcConnectionAccess = new NonContextualJdbcConnectionAccess( + readOnly, sessionEventsManager, factory.connectionProvider, this @@ -688,6 +697,7 @@ public JdbcConnectionAccess getJdbcConnectionAccess() { // we're using datasource-based multitenancy jdbcConnectionAccess = new ContextualJdbcConnectionAccess( tenantIdentifier, + readOnly, sessionEventsManager, factory.multiTenantConnectionProvider, this @@ -697,6 +707,14 @@ public JdbcConnectionAccess getJdbcConnectionAccess() { return jdbcConnectionAccess; } + private boolean manageReadOnly() { + return !factory.connectionProviderHandlesConnectionReadOnly(); + } + + private boolean manageSchema() { + return !factory.connectionProviderHandlesConnectionSchema(); + } + private boolean useSchemaBasedMultiTenancy() { return tenantIdentifier != null && getSessionFactoryOptions().getTenantSchemaMapper() != null; @@ -716,17 +734,23 @@ private String normalizeSchemaName(String schemaName) { @Override public void afterObtainConnection(Connection connection) throws SQLException { - if ( useSchemaBasedMultiTenancy() ) { + if ( useSchemaBasedMultiTenancy() && manageSchema() ) { initialSchema = connection.getSchema(); connection.setSchema( tenantSchema() ); } + if ( readOnly && manageReadOnly() ) { + connection.setReadOnly( true ); + } } @Override public void beforeReleaseConnection(Connection connection) throws SQLException { - if ( useSchemaBasedMultiTenancy() ) { + if ( useSchemaBasedMultiTenancy() && manageSchema() ) { connection.setSchema( initialSchema ); } + if ( readOnly && manageReadOnly() ) { + connection.setReadOnly( false ); + } } @Override 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 003c99ed45b8..d1940a42409b 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/ContextualJdbcConnectionAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/ContextualJdbcConnectionAccess.java @@ -13,14 +13,13 @@ import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.event.monitor.spi.EventMonitor; -import org.hibernate.event.monitor.spi.DiagnosticEvent; /** * @author Steve Ebersole */ public class ContextualJdbcConnectionAccess implements JdbcConnectionAccess, Serializable { private final Object tenantIdentifier; + private final boolean readOnly; private final SessionEventListener listener; private final MultiTenantConnectionProvider connectionProvider; private final SharedSessionContractImplementor session; @@ -28,10 +27,12 @@ public class ContextualJdbcConnectionAccess implements JdbcConnectionAccess, Ser public ContextualJdbcConnectionAccess( Object tenantIdentifier, + boolean readOnly, SessionEventListener listener, MultiTenantConnectionProvider connectionProvider, SharedSessionContractImplementor session) { this.tenantIdentifier = tenantIdentifier; + this.readOnly = readOnly; this.listener = listener; this.connectionProvider = connectionProvider; this.session = session; @@ -43,11 +44,13 @@ public Connection obtainConnection() throws SQLException { throw new HibernateException( "Tenant identifier required" ); } - final EventMonitor eventMonitor = session.getEventMonitor(); - final DiagnosticEvent connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent(); + final var eventMonitor = session.getEventMonitor(); + final var connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent(); try { listener.jdbcConnectionAcquisitionStart(); - return connectionProvider.getConnection( tenantIdentifier ); + return readOnly + ? connectionProvider.getReadOnlyConnection( tenantIdentifier ) + : connectionProvider.getConnection( tenantIdentifier ); } finally { eventMonitor.completeJdbcConnectionAcquisitionEvent( connectionAcquisitionEvent, session, tenantIdentifier ); @@ -61,11 +64,16 @@ public void releaseConnection(Connection connection) throws SQLException { throw new HibernateException( "Tenant identifier required" ); } - final EventMonitor eventMonitor = session.getEventMonitor(); - final DiagnosticEvent connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent(); + final var eventMonitor = session.getEventMonitor(); + final var connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent(); try { listener.jdbcConnectionReleaseStart(); - connectionProvider.releaseConnection( tenantIdentifier, connection ); + if ( readOnly ) { + connectionProvider.releaseReadOnlyConnection( tenantIdentifier, connection ); + } + else { + connectionProvider.releaseConnection( tenantIdentifier, connection ); + } } finally { eventMonitor.completeJdbcConnectionReleaseEvent( connectionReleaseEvent, session, tenantIdentifier ); 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 96ae39e771e3..74c7c35fce53 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/NonContextualJdbcConnectionAccess.java @@ -13,23 +13,24 @@ import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.event.monitor.spi.EventMonitor; -import org.hibernate.event.monitor.spi.DiagnosticEvent; /** * @author Steve Ebersole */ public class NonContextualJdbcConnectionAccess implements JdbcConnectionAccess, Serializable { + private final boolean readOnly; private final SessionEventListener listener; private final ConnectionProvider connectionProvider; private final SharedSessionContractImplementor session; public NonContextualJdbcConnectionAccess( + boolean readOnly, SessionEventListener listener, ConnectionProvider connectionProvider, SharedSessionContractImplementor session) { Objects.requireNonNull( listener ); Objects.requireNonNull( connectionProvider ); + this.readOnly = readOnly; this.listener = listener; this.connectionProvider = connectionProvider; this.session = session; @@ -37,11 +38,13 @@ public NonContextualJdbcConnectionAccess( @Override public Connection obtainConnection() throws SQLException { - final EventMonitor eventMonitor = session.getEventMonitor(); - final DiagnosticEvent connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent(); + final var eventMonitor = session.getEventMonitor(); + final var connectionAcquisitionEvent = eventMonitor.beginJdbcConnectionAcquisitionEvent(); try { listener.jdbcConnectionAcquisitionStart(); - return connectionProvider.getConnection(); + return readOnly + ? connectionProvider.getReadOnlyConnection() + : connectionProvider.getConnection(); } finally { eventMonitor.completeJdbcConnectionAcquisitionEvent( connectionAcquisitionEvent, session, null ); @@ -51,11 +54,16 @@ public Connection obtainConnection() throws SQLException { @Override public void releaseConnection(Connection connection) throws SQLException { - final EventMonitor eventMonitor = session.getEventMonitor(); - final DiagnosticEvent connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent(); + final var eventMonitor = session.getEventMonitor(); + final var connectionReleaseEvent = eventMonitor.beginJdbcConnectionReleaseEvent(); try { listener.jdbcConnectionReleaseStart(); - connectionProvider.closeConnection( connection ); + if ( readOnly ) { + connectionProvider.closeReadOnlyConnection( connection ); + } + else { + connectionProvider.closeConnection( connection ); + } } finally { eventMonitor.completeJdbcConnectionReleaseEvent( connectionReleaseEvent, session, null ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionCreationOptions.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionCreationOptions.java index 2ca502eeda59..98ee29bcb9a8 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionCreationOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionCreationOptions.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.TimeZone; +import org.hibernate.CacheMode; import org.hibernate.FlushMode; import org.hibernate.Interceptor; import org.hibernate.SessionEventListener; @@ -46,6 +47,10 @@ public interface SessionCreationOptions { Object getTenantIdentifierValue(); + boolean isReadOnly(); + + CacheMode getInitialCacheMode(); + boolean isIdentifierRollbackEnabled(); TimeZone getJdbcTimeZone(); 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 f88d72715840..5fed6ff1e3dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java @@ -25,6 +25,7 @@ import javax.naming.StringRefAddr; import jakarta.persistence.TypedQuery; +import org.hibernate.CacheMode; import org.hibernate.ConnectionAcquisitionMode; import org.hibernate.ConnectionReleaseMode; import org.hibernate.CustomEntityDirtinessStrategy; @@ -1126,6 +1127,8 @@ public static class SessionBuilderImpl implements SessionBuilderImplementor, Ses private boolean autoClose; private boolean autoClear; private Object tenantIdentifier; + private boolean readOnly; + private CacheMode cacheMode; private boolean identifierRollback; private TimeZone jdbcTimeZone; private boolean explicitNoInterceptor; @@ -1144,20 +1147,21 @@ public SessionBuilderImpl(SessionFactoryImpl sessionFactory) { this.sessionFactory = sessionFactory; // set up default builder values... - final SessionFactoryOptions sessionFactoryOptions = sessionFactory.getSessionFactoryOptions(); - statementInspector = sessionFactoryOptions.getStatementInspector(); - connectionHandlingMode = sessionFactoryOptions.getPhysicalConnectionHandlingMode(); - autoClose = sessionFactoryOptions.isAutoCloseSessionEnabled(); - defaultBatchFetchSize = sessionFactoryOptions.getDefaultBatchFetchSize(); - subselectFetchEnabled = sessionFactoryOptions.isSubselectFetchEnabled(); - identifierRollback = sessionFactoryOptions.isIdentifierRollbackEnabled(); + final var options = sessionFactory.getSessionFactoryOptions(); + statementInspector = options.getStatementInspector(); + connectionHandlingMode = options.getPhysicalConnectionHandlingMode(); + autoClose = options.isAutoCloseSessionEnabled(); + defaultBatchFetchSize = options.getDefaultBatchFetchSize(); + subselectFetchEnabled = options.isSubselectFetchEnabled(); + identifierRollback = options.isIdentifierRollbackEnabled(); + cacheMode = options.getInitialSessionCacheMode(); final var currentTenantIdentifierResolver = sessionFactory.getCurrentTenantIdentifierResolver(); if ( currentTenantIdentifierResolver != null ) { tenantIdentifier = currentTenantIdentifierResolver.resolveCurrentTenantIdentifier(); } - jdbcTimeZone = sessionFactoryOptions.getJdbcTimeZone(); + jdbcTimeZone = options.getJdbcTimeZone(); } @@ -1234,6 +1238,16 @@ public Object getTenantIdentifierValue() { return tenantIdentifier; } + @Override + public boolean isReadOnly() { + return readOnly; + } + + @Override + public CacheMode getInitialCacheMode() { + return cacheMode; + } + @Override public boolean isIdentifierRollbackEnabled() { return identifierRollback; @@ -1279,7 +1293,7 @@ public SessionBuilderImpl statementInspector(StatementInspector statementInspect } @Override - public SessionBuilder statementInspector(UnaryOperator operator) { + public SessionBuilderImpl statementInspector(UnaryOperator operator) { this.statementInspector = operator::apply; return this; } @@ -1297,7 +1311,7 @@ public SessionBuilderImpl connectionHandlingMode(PhysicalConnectionHandlingMode } @Override - public SessionBuilder connectionHandling(ConnectionAcquisitionMode acquisitionMode, ConnectionReleaseMode releaseMode) { + public SessionBuilderImpl connectionHandling(ConnectionAcquisitionMode acquisitionMode, ConnectionReleaseMode releaseMode) { this.connectionHandlingMode = PhysicalConnectionHandlingMode.interpret( acquisitionMode, releaseMode); return this; } @@ -1339,7 +1353,19 @@ public SessionBuilderImpl tenantIdentifier(Object tenantIdentifier) { } @Override - public SessionBuilder identifierRollback(boolean identifierRollback) { + public SessionBuilderImpl readOnly(boolean readOnly) { + this.readOnly = readOnly; + return this; + } + + @Override + public SessionBuilder initialCacheMode(CacheMode cacheMode) { + this.cacheMode = cacheMode; + return this; + } + + @Override + public SessionBuilderImpl identifierRollback(boolean identifierRollback) { this.identifierRollback = identifierRollback; return this; } @@ -1380,10 +1406,14 @@ public static class StatelessSessionBuilderImpl implements StatelessSessionBuild private StatementInspector statementInspector; private Connection connection; private Object tenantIdentifier; + private boolean readOnly; + private CacheMode cacheMode; public StatelessSessionBuilderImpl(SessionFactoryImpl sessionFactory) { this.sessionFactory = sessionFactory; - this.statementInspector = sessionFactory.getSessionFactoryOptions().getStatementInspector(); + final var options = sessionFactory.getSessionFactoryOptions(); + statementInspector = options.getStatementInspector(); + cacheMode = options.getInitialSessionCacheMode(); final var tenantIdentifierResolver = sessionFactory.getCurrentTenantIdentifierResolver(); if ( tenantIdentifierResolver != null ) { @@ -1414,6 +1444,18 @@ public StatelessSessionBuilder tenantIdentifier(Object tenantIdentifier) { return this; } + @Override + public StatelessSessionBuilder readOnly(boolean readOnly) { + this.readOnly = readOnly; + return this; + } + + @Override + public StatelessSessionBuilder initialCacheMode(CacheMode cacheMode) { + this.cacheMode = cacheMode; + return this; + } + @Override @Deprecated public StatelessSessionBuilder statementInspector(StatementInspector statementInspector) { this.statementInspector = statementInspector; @@ -1489,6 +1531,16 @@ public String getTenantIdentifier() { : sessionFactory.getTenantIdentifierJavaType().toString( tenantIdentifier ); } + @Override + public boolean isReadOnly() { + return readOnly; + } + + @Override + public CacheMode getInitialCacheMode() { + return cacheMode; + } + @Override public Object getTenantIdentifierValue() { return tenantIdentifier; @@ -1525,6 +1577,17 @@ public JavaType getTenantIdentifierJavaType() { return tenantIdentifierJavaType; } + boolean connectionProviderHandlesConnectionReadOnly() { + return multiTenantConnectionProvider != null + ? multiTenantConnectionProvider.handlesConnectionReadOnly() + : connectionProvider.handlesConnectionReadOnly(); + } + + boolean connectionProviderHandlesConnectionSchema() { + return multiTenantConnectionProvider != null + ? multiTenantConnectionProvider.handlesConnectionSchema() + : connectionProvider.handlesConnectionSchema(); + } // Serialization handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index a5c9b5d47449..80c918de37e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -192,7 +192,7 @@ public SessionImpl(SessionFactoryImpl factory, SessionCreationOptions options) { final var sessionOpenEvent = getEventMonitor().beginSessionOpenEvent(); try { - persistenceContext = createPersistenceContext(); + persistenceContext = createPersistenceContext( options ); actionQueue = createActionQueue(); eventListenerGroups = factory.getEventListenerGroups(); @@ -252,8 +252,10 @@ private FlushMode getInitialFlushMode() { : ConfigurationHelper.getFlushMode( getSessionProperty( HINT_FLUSH_MODE ), FlushMode.AUTO ); } - protected PersistenceContext createPersistenceContext() { - return PersistenceContexts.createPersistenceContext( this ); + protected PersistenceContext createPersistenceContext(SessionCreationOptions options) { + final var persistenceContext = PersistenceContexts.createPersistenceContext( this ); + persistenceContext.setDefaultReadOnly( options.isReadOnly() ); + return persistenceContext; } protected ActionQueue createActionQueue() { @@ -1901,6 +1903,9 @@ public boolean isDefaultReadOnly() { @Override public void setDefaultReadOnly(boolean defaultReadOnly) { + if ( !defaultReadOnly && isReadOnly() ) { + throw new SessionException( "Session was created in read-only mode" ); + } persistenceContext.setDefaultReadOnly( defaultReadOnly ); } @@ -2029,6 +2034,7 @@ private static class SharedSessionBuilderImpl private final SessionImpl session; private boolean shareTransactionContext; private boolean tenantIdChanged; + private boolean readOnlyChanged; private SharedSessionBuilderImpl(SessionImpl session) { super( (SessionFactoryImpl) session.getFactory() ); @@ -2040,8 +2046,15 @@ private SharedSessionBuilderImpl(SessionImpl session) { @Override public SessionImpl openSession() { if ( session.getSessionFactoryOptions().isMultiTenancyEnabled() ) { - if ( tenantIdChanged && shareTransactionContext ) { - throw new SessionException( "Cannot redefine the tenant identifier on a child session if the connection is reused" ); + if ( shareTransactionContext ) { + if ( tenantIdChanged ) { + throw new SessionException( + "Cannot redefine the tenant identifier on a child session if the connection is reused" ); + } + if ( readOnlyChanged ) { + throw new SessionException( + "Cannot redefine the read-only mode on a child session if the connection is reused" ); + } } } return super.openSession(); @@ -2065,6 +2078,19 @@ public SharedSessionBuilderImpl tenantIdentifier(Object tenantIdentifier) { return this; } + @Override + public SharedSessionBuilderImpl readOnly(boolean readOnly) { + super.readOnly( readOnly ); + readOnlyChanged = true; + return this; + } + + @Override + public SharedSessionBuilderImpl initialCacheMode(CacheMode cacheMode) { + super.initialCacheMode( cacheMode ); + return this; + } + @Override public SharedSessionBuilderImpl interceptor() { super.interceptor( session.getInterceptor() ); @@ -2182,7 +2208,7 @@ public SharedSessionBuilderImpl statementInspector(StatementInspector statementI } @Override - public SessionBuilder statementInspector(UnaryOperator operator) { + public SharedSessionBuilderImpl statementInspector(UnaryOperator operator) { super.statementInspector(operator); return this; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/delegation/TestDelegatingSessionBuilder.java b/hibernate-core/src/test/java/org/hibernate/orm/test/delegation/TestDelegatingSessionBuilder.java index 39bf48ad9bc2..7085aca12499 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/delegation/TestDelegatingSessionBuilder.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/delegation/TestDelegatingSessionBuilder.java @@ -19,7 +19,6 @@ @SuppressWarnings("unused") public class TestDelegatingSessionBuilder extends AbstractDelegatingSessionBuilder { - @SuppressWarnings("rawtypes") public TestDelegatingSessionBuilder(SessionBuilder delegate) { super( delegate ); }