Skip to content

Commit

Permalink
Support for multi tenancy column discriminator quarkusio#28644
Browse files Browse the repository at this point in the history
  • Loading branch information
humcqc authored and yrodiere committed May 1, 2023
1 parent 9380d09 commit 143a61b
Show file tree
Hide file tree
Showing 23 changed files with 980 additions and 20 deletions.
24 changes: 22 additions & 2 deletions docs/src/main/asciidoc/hibernate-orm.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ include::_attributes.adoc[]
:categories: data
:summary: Hibernate ORM is the de facto Jakarta Persistence implementation and offers you the full breath of an Object Relational Mapper. It works beautifully in Quarkus.
:config-file: application.properties
:orm-doc-url-prefix: https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html
:orm-doc-url-prefix: https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html

Hibernate ORM is the de facto standard Jakarta Persistence (formerly known as JPA) implementation and offers you the full breadth of an Object Relational Mapper.
It works beautifully in Quarkus.
Expand Down Expand Up @@ -916,7 +916,7 @@ Jump over to xref:datasource.adoc[Quarkus - Datasources] for all details.

"The term multitenancy, in general, is applied to software development to indicate an architecture in which a single running instance of an application simultaneously serves multiple clients (tenants). This is highly common in SaaS solutions. Isolating information (data, customizations, etc.) pertaining to the various tenants is a particular challenge in these systems. This includes the data owned by each tenant stored in the database" (link:{orm-doc-url-prefix}#multitenacy[Hibernate User Guide]).

Quarkus currently supports the link:{orm-doc-url-prefix}#multitenacy-separate-database[separate database] and the link:{orm-doc-url-prefix}#multitenacy-separate-schema[separate schema] approach.
Quarkus currently supports the link:{orm-doc-url-prefix}#multitenacy-separate-database[separate database] approach, the link:{orm-doc-url-prefix}#multitenacy-separate-schema[separate schema] approach and the link:{orm-doc-url-prefix}#multitenacy-discriminator[discriminator] approach.

To see multitenancy in action, you can check out the {quickstarts-tree-url}/hibernate-orm-multi-tenancy-quickstart[hibernate-orm-multi-tenancy-quickstart] quickstart.

Expand Down Expand Up @@ -1147,6 +1147,26 @@ INSERT INTO known_fruits(id, name) VALUES (2, 'Apricots');
INSERT INTO known_fruits(id, name) VALUES (3, 'Blackberries');
----



==== DISCRIMINATOR approach

The default data source will be used for all tenants. All entities defining a field annotated with `@TenantId` will have that field populated automatically, and will get filtered automatically in queries.


[source,properties]
----
# Enable DISCRIMINATOR approach
quarkus.hibernate-orm.multitenant=DISCRIMINATOR
# The default data source used for all tenant schemas
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_test
----


=== Programmatically Resolving Tenants Connections

If you need a more dynamic configuration for the different tenants you want to support and don't want to end up with multiple entries in your configuration file,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1489,11 +1489,6 @@ private static MultiTenancyStrategy getMultiTenancyStrategy(Optional<String> mul
final MultiTenancyStrategy multiTenancyStrategy = MultiTenancyStrategy
.valueOf(multitenancyStrategy.orElse(MultiTenancyStrategy.NONE.name())
.toUpperCase(Locale.ROOT));
if (multiTenancyStrategy == MultiTenancyStrategy.DISCRIMINATOR) {
// See https://hibernate.atlassian.net/browse/HHH-6054
throw new ConfigurationException("The Hibernate ORM multitenancy strategy "
+ MultiTenancyStrategy.DISCRIMINATOR + " is currently not supported");
}
return multiTenancyStrategy;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String
persistenceUnitName,
standardServiceRegistry /* Mostly ignored! (yet needs to match) */,
runtimeSettings,
validatorFactory, cdiBeanManager);
validatorFactory, cdiBeanManager, recordedState.getMultiTenancyStrategy());
}

log.debug("Found no matching persistence units");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import io.quarkus.arc.InjectableInstance;
import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil;
import io.quarkus.hibernate.orm.runtime.RuntimeSettings;
import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy;
import io.quarkus.hibernate.orm.runtime.observers.QuarkusSessionFactoryObserverForDbVersionCheck;
import io.quarkus.hibernate.orm.runtime.observers.SessionFactoryObserverForNamedQueryValidation;
import io.quarkus.hibernate.orm.runtime.observers.SessionFactoryObserverForSchemaExport;
Expand All @@ -49,16 +50,19 @@ public class FastBootEntityManagerFactoryBuilder implements EntityManagerFactory
private final Object validatorFactory;
private final Object cdiBeanManager;

protected final MultiTenancyStrategy multiTenancyStrategy;

public FastBootEntityManagerFactoryBuilder(
PrevalidatedQuarkusMetadata metadata, String persistenceUnitName,
StandardServiceRegistry standardServiceRegistry, RuntimeSettings runtimeSettings, Object validatorFactory,
Object cdiBeanManager) {
Object cdiBeanManager, MultiTenancyStrategy multiTenancyStrategy) {
this.metadata = metadata;
this.persistenceUnitName = persistenceUnitName;
this.standardServiceRegistry = standardServiceRegistry;
this.runtimeSettings = runtimeSettings;
this.validatorFactory = validatorFactory;
this.cdiBeanManager = cdiBeanManager;
this.multiTenancyStrategy = multiTenancyStrategy;
}

@Override
Expand All @@ -76,7 +80,8 @@ public EntityManagerFactory build() {
try {
final SessionFactoryOptionsBuilder optionsBuilder = metadata.buildSessionFactoryOptionsBuilder();
populate(persistenceUnitName, optionsBuilder, standardServiceRegistry);
return new SessionFactoryImpl(metadata, optionsBuilder.buildOptions());
return new SessionFactoryImpl(metadata, optionsBuilder.buildOptions(),
metadata.getTypeConfiguration().getMetadataBuildingContext().getBootstrapContext());
} catch (Exception e) {
throw persistenceException("Unable to build Hibernate SessionFactory", e);
}
Expand Down Expand Up @@ -189,7 +194,9 @@ protected void populate(String persistenceUnitName, SessionFactoryOptionsBuilder
BytecodeProvider bytecodeProvider = ssr.getService(BytecodeProvider.class);
options.addSessionFactoryObservers(new SessionFactoryObserverForBytecodeEnhancer(bytecodeProvider));

if (options.isMultiTenancyEnabled()) {
// Should be added in case of discriminator strategy too, that is not handled by options.isMultiTenancyEnabled()
if (options.isMultiTenancyEnabled()
|| (multiTenancyStrategy != null && multiTenancyStrategy != MultiTenancyStrategy.NONE)) {
options.applyCurrentTenantIdentifierResolver(new HibernateCurrentTenantIdentifierResolver(persistenceUnitName));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ public class FastBootMetadataBuilder {
private final Collection<Class<? extends Integrator>> additionalIntegrators;
private final Collection<ProvidedService<?>> providedServices;
private final PreGeneratedProxies preGeneratedProxies;
private final Optional<String> dataSource;
private final MultiTenancyStrategy multiTenancyStrategy;
private final boolean isReactive;
private final boolean fromPersistenceXml;
private final List<HibernateOrmIntegrationStaticDescriptor> integrationStaticDescriptors;
Expand All @@ -121,7 +121,6 @@ public class FastBootMetadataBuilder {
public FastBootMetadataBuilder(final QuarkusPersistenceUnitDefinition puDefinition, Scanner scanner,
Collection<Class<? extends Integrator>> additionalIntegrators, PreGeneratedProxies preGeneratedProxies) {
this.persistenceUnit = puDefinition.getActualHibernateDescriptor();
this.dataSource = puDefinition.getConfig().getDataSource();
this.isReactive = puDefinition.isReactive();
this.fromPersistenceXml = puDefinition.isFromPersistenceXml();
this.additionalIntegrators = additionalIntegrators;
Expand All @@ -136,6 +135,8 @@ public FastBootMetadataBuilder(final QuarkusPersistenceUnitDefinition puDefiniti

final RecordableBootstrap ssrBuilder = RecordableBootstrapFactory.createRecordableBootstrapBuilder(puDefinition);

// Should be set before calling mergeSettings()
this.multiTenancyStrategy = puDefinition.getConfig().getMultiTenancyStrategy();
final MergedSettings mergedSettings = mergeSettings(puDefinition);
this.buildTimeSettings = createBuildTimeSettings(puDefinition, mergedSettings.getConfigurationValues());

Expand Down Expand Up @@ -198,6 +199,7 @@ public FastBootMetadataBuilder(final QuarkusPersistenceUnitDefinition puDefiniti
// for the time being we want to revoke access to the temp ClassLoader if one
// was passed
metamodelBuilder.applyTempClassLoader(null);

}

private BuildTimeSettings createBuildTimeSettings(QuarkusPersistenceUnitDefinition puDefinition,
Expand Down Expand Up @@ -248,8 +250,8 @@ private MergedSettings mergeSettings(QuarkusPersistenceUnitDefinition puDefiniti

cfg.put(PERSISTENCE_UNIT_NAME, persistenceUnit.getName());

MultiTenancyStrategy multiTenancyStrategy = puDefinition.getConfig().getMultiTenancyStrategy();
if (multiTenancyStrategy != null && multiTenancyStrategy != MultiTenancyStrategy.NONE) {
if (multiTenancyStrategy != null && multiTenancyStrategy != MultiTenancyStrategy.NONE
&& multiTenancyStrategy != MultiTenancyStrategy.DISCRIMINATOR) {
// We need to initialize the multi tenant connection provider
// on static init as it is used in MetadataBuildingOptionsImpl
// to determine if multi-tenancy is enabled.
Expand Down Expand Up @@ -423,7 +425,7 @@ public RecordedState build() {
destroyServiceRegistry();
ProxyDefinitions proxyClassDefinitions = ProxyDefinitions.createFromMetadata(storeableMetadata, preGeneratedProxies);
return new RecordedState(dialect, storeableMetadata, buildTimeSettings, getIntegrators(),
providedServices, integrationSettingsBuilder.build(), proxyClassDefinitions,
providedServices, integrationSettingsBuilder.build(), proxyClassDefinitions, multiTenancyStrategy,
isReactive, fromPersistenceXml);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import io.quarkus.hibernate.orm.runtime.BuildTimeSettings;
import io.quarkus.hibernate.orm.runtime.IntegrationSettings;
import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy;
import io.quarkus.hibernate.orm.runtime.proxies.ProxyDefinitions;

public final class RecordedState {
Expand All @@ -19,13 +20,15 @@ public final class RecordedState {
private final Collection<ProvidedService<?>> providedServices;
private final IntegrationSettings integrationSettings;
private final ProxyDefinitions proxyClassDefinitions;
private final MultiTenancyStrategy multiTenancyStrategy;

private final boolean isReactive;
private final boolean fromPersistenceXml;

public RecordedState(Dialect dialect, PrevalidatedQuarkusMetadata metadata,
BuildTimeSettings settings, Collection<Integrator> integrators,
Collection<ProvidedService<?>> providedServices, IntegrationSettings integrationSettings,
ProxyDefinitions classDefinitions,
ProxyDefinitions classDefinitions, MultiTenancyStrategy strategy,
boolean isReactive, boolean fromPersistenceXml) {
this.dialect = dialect;
this.metadata = metadata;
Expand All @@ -34,6 +37,7 @@ public RecordedState(Dialect dialect, PrevalidatedQuarkusMetadata metadata,
this.providedServices = providedServices;
this.integrationSettings = integrationSettings;
this.proxyClassDefinitions = classDefinitions;
this.multiTenancyStrategy = strategy;
this.isReactive = isReactive;
this.fromPersistenceXml = fromPersistenceXml;
}
Expand Down Expand Up @@ -66,6 +70,10 @@ public ProxyDefinitions getProxyClassDefinitions() {
return proxyClassDefinitions;
}

public MultiTenancyStrategy getMultiTenancyStrategy() {
return multiTenancyStrategy;
}

public boolean isReactive() {
return isReactive;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String
persistenceUnitName,
standardServiceRegistry /* Mostly ignored! (yet needs to match) */,
runtimeSettings,
validatorFactory, cdiBeanManager);
validatorFactory, cdiBeanManager, recordedState.getMultiTenancyStrategy());
}

log.debug("Found no matching persistence units");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@
import io.quarkus.hibernate.orm.runtime.PersistenceUnitUtil;
import io.quarkus.hibernate.orm.runtime.RuntimeSettings;
import io.quarkus.hibernate.orm.runtime.boot.FastBootEntityManagerFactoryBuilder;
import io.quarkus.hibernate.orm.runtime.migration.MultiTenancyStrategy;
import io.quarkus.hibernate.orm.runtime.recording.PrevalidatedQuarkusMetadata;

public final class FastBootReactiveEntityManagerFactoryBuilder extends FastBootEntityManagerFactoryBuilder {

public FastBootReactiveEntityManagerFactoryBuilder(PrevalidatedQuarkusMetadata metadata, String persistenceUnitName,
StandardServiceRegistry standardServiceRegistry, RuntimeSettings runtimeSettings, Object validatorFactory,
Object cdiBeanManager) {
super(metadata, persistenceUnitName, standardServiceRegistry, runtimeSettings, validatorFactory, cdiBeanManager);
Object cdiBeanManager, MultiTenancyStrategy strategy) {
super(metadata, persistenceUnitName, standardServiceRegistry, runtimeSettings, validatorFactory,
cdiBeanManager, strategy);
}

@Override
Expand Down
Loading

0 comments on commit 143a61b

Please sign in to comment.