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:
+ *
+ *
a read-only session will connect to a read-only replica, but
+ *
a non-read-only session will connect to a writable replica.
+ *
+ *
+ * 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:
+ *
+ *
a read-only session will connect to a read-only replica, but
+ *
a non-read-only session will connect to a writable replica.
+ *
+ *
+ * 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