Skip to content
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

Support for multi tenancy column discriminator #32855

Merged
merged 1 commit into from
May 1, 2023
Merged
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
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());
humcqc marked this conversation as resolved.
Show resolved Hide resolved
} 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