Skip to content

HHH-19708 prototype support for read/write replicas #10754

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions hibernate-core/src/main/java/org/hibernate/Session.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
54 changes: 54 additions & 0 deletions hibernate-core/src/main/java/org/hibernate/SessionBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* Furthermore, if read/write replication is in use, then:
* <ul>
* <li>a read-only session will connect to a read-only replica, but
* <li>a non-read-only session will connect to a writable replica.
* </ul>
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/**
Expand All @@ -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.
* <p>
* Furthermore, if read/write replication is in use, then:
* <ul>
* <li>a read-only session will connect to a read-only replica, but
* <li>a non-read-only session will connect to a writable replica.
* </ul>
* <p>
* 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
* <p>
* 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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 );
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<Object> getTenantIdentifierResolver(
Map<String,Object> 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<Object> getTenantSchemaMapper(
Map<String,Object> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading