Skip to content

HHH-19559, HHH-19580 schema-based multitenancy #10424

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

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
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
53 changes: 34 additions & 19 deletions documentation/src/main/asciidoc/introduction/Advanced.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -268,27 +268,56 @@ var entityManager =

However, since we often don't have this level of control over creation of the session, it's more common to supply an implementation of link:{doc-javadoc-url}org/hibernate/context/spi/CurrentTenantIdentifierResolver.html[`CurrentTenantIdentifierResolver`] to Hibernate.

There are three common ways to implement multi-tenancy:
To make use of multi-tenancy, we'll usually need to set at least one of these configuration properties:

.Multi-tenancy configuration
[%breakable,cols="36,~"]
|===
| Configuration property name | Purpose

| link:{doc-javadoc-url}org/hibernate/cfg/MultiTenancySettings.html#MULTI_TENANT_IDENTIFIER_RESOLVER[`hibernate.tenant_identifier_resolver`] | Specifies the `CurrentTenantIdentifierResolver`
| link:{doc-javadoc-url}org/hibernate/cfg/MultiTenancySettings.html#MULTI_TENANT_SCHEMA_MAPPER[`hibernate.multi_tenant.schema_mapper`] | Specifies the `TenantSchemaMapper` for schema-based multi-tenancy
| link:{doc-javadoc-url}org/hibernate/cfg/MultiTenancySettings.html#MULTI_TENANT_CONNECTION_PROVIDER[`hibernate.multi_tenant_connection_provider`] | Specifies the `MultiTenantConnectionProvider` for database-based multi-tenancy
|===

// Do not configure those properties if you would like the configured `BeanContainer` provide the implementation.

There are three common approaches to multi-tenancy:

1. each tenant has its own database,
2. each tenant has its own schema, or
3. tenants share tables in a single schema, and rows are tagged with the tenant id.

From the point of view of Hibernate, there's little difference between the first two options.
Hibernate will need to obtain a JDBC connection with permissions on the database and schema owned by the current tenant.
[discrete]
==== Database-based multi-tenancy

Therefore, we must implement a link:{doc-javadoc-url}org/hibernate/engine/jdbc/connections/spi/MultiTenantConnectionProvider.html[`MultiTenantConnectionProvider`] which takes on this responsibility:
The first option is to give each tenant its own database.
That is, we'll use a separate source of JDBC connections for each tenant.

The interface link:{doc-javadoc-url}org/hibernate/engine/jdbc/connections/spi/MultiTenantConnectionProvider.html[`MultiTenantConnectionProvider`] is responsible for obtaining an appropriate `Connection` for a given tenant.
Typically, we'll provide a custom implementation of this interface:

- from time to time, Hibernate will ask for a connection, passing the id of the current tenant, and then we must create an appropriate connection or obtain one from a pool, and return it to Hibernate, and
- later, Hibernate will release the connection and ask us to destroy it or return it to the appropriate pool.

[TIP]
====
Check out link:{doc-javadoc-url}org/hibernate/engine/jdbc/connections/spi/DataSourceBasedMultiTenantConnectionProviderImpl.html[`DataSourceBasedMultiTenantConnectionProviderImpl`] for inspiration.
If your source of JDBC connections is a set of JNDI-bound ``DataSource``s, you might even be able to use this implementation directly.
====

[discrete]
==== Schema-based multi-tenancy

The second option is to keep all the data for different tenants in the same database, giving each tenant a different named database schema with its own set of tables.

In this case we must supply a link:{doc-javadoc-url}org/hibernate/context/spi/TenantSchemaMapper.html[`TenantSchemaMapper`] which is responsible for mapping from tenant ids to schema names.

[discrete]
==== Discriminator-based multi-tenancy

The third option is quite different.
In this case we don't need a `MultiTenantConnectionProvider`, but we will need a dedicated column holding the tenant id mapped by each of our entities.
In this case we store data from all tenants in the same tables, but each table has a dedicated column holding the tenant id mapped by each of our entities.

[source,java]
----
Expand All @@ -309,20 +338,6 @@ Within a given session, our data is automatically filtered so that only rows tag
Native SQL queries are _not_ automatically filtered by tenant id; you'll have to do that part yourself.
====

To make use of multi-tenancy, we'll usually need to set at least one of these configuration properties:

.Multi-tenancy configuration
[%breakable,cols="36,~"]
|===
| Configuration property name | Purpose

| link:{doc-javadoc-url}org/hibernate/cfg/MultiTenancySettings.html#MULTI_TENANT_IDENTIFIER_RESOLVER[`hibernate.tenant_identifier_resolver`] | Specifies the `CurrentTenantIdentifierResolver`
| link:{doc-javadoc-url}org/hibernate/cfg/MultiTenancySettings.html#MULTI_TENANT_CONNECTION_PROVIDER[`hibernate.multi_tenant_connection_provider`] | Specifies the `MultiTenantConnectionProvider`
|===

Do not configure those properties if you would like the configured `BeanContainer` provide the implementation.
A longer discussion of multi-tenancy may be found in the {multitenacy-doc}[User Guide].

[[custom-sql]]
=== Using custom-written SQL

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 );
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.TenantSchemaMapper;
import org.hibernate.type.TimeZoneStorageStrategy;
import org.hibernate.annotations.CacheLayout;
import org.hibernate.boot.SchemaAutoTooling;
Expand Down Expand Up @@ -187,6 +188,7 @@ public class SessionFactoryOptionsBuilder implements SessionFactoryOptions {
// multi-tenancy
private boolean multiTenancyEnabled;
private CurrentTenantIdentifierResolver<Object> currentTenantIdentifierResolver;
private TenantSchemaMapper<Object> tenantSchemaMapper;

// Queries
private SqmFunctionRegistry sqmFunctionRegistry;
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -1003,6 +1008,11 @@ public boolean isMultiTenancyEnabled() {
return multiTenancyEnabled;
}

@Override
public TenantSchemaMapper<Object> getTenantSchemaMapper() {
return tenantSchemaMapper;
}

@Override
public CurrentTenantIdentifierResolver<Object> getCurrentTenantIdentifierResolver() {
return currentTenantIdentifierResolver;
Expand Down Expand Up @@ -1450,6 +1460,11 @@ public void applyCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver
this.currentTenantIdentifierResolver = (CurrentTenantIdentifierResolver<Object>) resolver;
}

public void applyTenantSchemaMapper(TenantSchemaMapper<?> mapper) {
//noinspection unchecked
this.tenantSchemaMapper = (TenantSchemaMapper<Object>) mapper;
}

public void enableNamedQueryCheckingOnStartup(boolean enabled) {
this.namedQueryStartupCheckingEnabled = enabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -224,6 +225,11 @@ public CurrentTenantIdentifierResolver<Object> getCurrentTenantIdentifierResolve
return delegate.getCurrentTenantIdentifierResolver();
}

@Override
public TenantSchemaMapper<Object> getTenantSchemaMapper() {
return delegate.getTenantSchemaMapper();
}

@Override
public JavaType<Object> getDefaultTenantIdentifierJavaType() {
return delegate.getDefaultTenantIdentifierJavaType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -313,6 +314,18 @@ default SessionEventListener[] buildSessionEventListeners() {
*/
CurrentTenantIdentifierResolver<Object> 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<Object> getTenantSchemaMapper();

/**
* @see org.hibernate.cfg.TransactionSettings#JTA_TRACK_BY_THREAD
*/
Expand Down
33 changes: 30 additions & 3 deletions hibernate-core/src/main/java/org/hibernate/cfg/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -173,7 +174,8 @@ public class Configuration {
private EntityNotFoundDelegate entityNotFoundDelegate;
private SessionFactoryObserver sessionFactoryObserver;
private StatementInspector statementInspector;
private CurrentTenantIdentifierResolver<Object> currentTenantIdentifierResolver;
private CurrentTenantIdentifierResolver<?> currentTenantIdentifierResolver;
private TenantSchemaMapper<?> tenantSchemaMapper;
private CustomEntityDirtinessStrategy customEntityDirtinessStrategy;
private ColumnOrderingStrategy columnOrderingStrategy;
private SharedCacheMode sharedCacheMode;
Expand Down Expand Up @@ -939,7 +941,7 @@ public Configuration setStatementInspector(StatementInspector statementInspector
/**
* The {@link CurrentTenantIdentifierResolver}, if any, that was added to this configuration.
*/
public CurrentTenantIdentifierResolver<Object> getCurrentTenantIdentifierResolver() {
public CurrentTenantIdentifierResolver<?> getCurrentTenantIdentifierResolver() {
return currentTenantIdentifierResolver;
}

Expand All @@ -948,11 +950,32 @@ public CurrentTenantIdentifierResolver<Object> getCurrentTenantIdentifierResolve
*
* @return {@code this} for method chaining
*/
public Configuration setCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver<Object> 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.
*/
Expand Down Expand Up @@ -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 );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
* <ul>
* <li>an instance of {@code CurrentTenantIdentifierResolver},
* <li>a {@link Class} representing an class that implements {@code CurrentTenantIdentifierResolver}, or
* <li>a {@link Class} representing a class that implements {@code CurrentTenantIdentifierResolver}, or
* <li>the name of a class that implements {@code CurrentTenantIdentifierResolver}.
* </ul>
*
* @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}.
* <p/>
* 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:
* <ul>
* <li>an instance of {@code TenantSchemaMapper},
* <li>a {@link Class} representing a class that implements {@code TenantSchemaMapper}, or
* <li>the name of a class that implements {@code TenantSchemaMapper}.
* </ul>
* 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}.
* <p>
* 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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ public SessionFactoryImplementor factory() {

protected SessionBuilder baseSessionBuilder() {
final SessionBuilderImplementor builder = factory.withOptions();
final CurrentTenantIdentifierResolver<Object> resolver = factory.getCurrentTenantIdentifierResolver();
final var resolver = factory.getCurrentTenantIdentifierResolver();
if ( resolver != null ) {
builder.tenantIdentifier( resolver.resolveCurrentTenantIdentifier() );
}
return builder;
}

protected void validateExistingSession(Session existingSession) {
final CurrentTenantIdentifierResolver<Object> resolver = factory.getCurrentTenantIdentifierResolver();
final var resolver = factory.getCurrentTenantIdentifierResolver();
if ( resolver != null && resolver.validateExistingCurrentSessions() ) {
final Object currentValue = resolver.resolveCurrentTenantIdentifier();
final JavaType<Object> tenantIdentifierJavaType = factory.getTenantIdentifierJavaType();
Expand Down
Loading