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 committed Apr 29, 2023
1 parent a5d8660 commit 1bad0d0
Show file tree
Hide file tree
Showing 18 changed files with 845 additions and 19 deletions.
51 changes: 49 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], 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,53 @@ 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 `@TenandId` will be handle automatically.


[source,properties]
----
# Disable generation
quarkus.hibernate-orm.database.generation=none
# Enable DISCRIMINATOR approach and use default datasource
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
# Enable Flyway configuration
quarkus.flyway.migrate-at-start=true
----

Here is an example of the Flyway SQL (`V1.0.0__create_fruits.sql`) to be created in the configured folder `src/main/resources/schema`.

[source,sql]
----
CREATE TABLE known_fruits
(
tenantId VARCHAR(40),
id INT,
name VARCHAR(40)
);
CREATE SEQUENCE known_fruits_id_seq START WITH 7;
INSERT INTO known_fruits(tenantId, id, name) VALUES ('base', 1, 'Cherry');
INSERT INTO known_fruits(tenantId, id, name) VALUES ('base', 2, 'Apple');
INSERT INTO known_fruits(tenantId, id, name) VALUES ('base', 3, 'Banana');
INSERT INTO known_fruits(tenantId, id, name) VALUES ('mycompany', 4, 'Avocado');
INSERT INTO known_fruits(tenantId, id, name) VALUES ('mycompany', 5, 'Apricots');
INSERT INTO known_fruits(tenantId, id, name) VALUES ('mycompany', 6, 'Blackberries');
----



=== 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 Down Expand Up @@ -198,6 +197,10 @@ 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);

final MultiTenancyStrategy strategy = puDefinition.getConfig().getMultiTenancyStrategy();
this.multiTenancyStrategy = strategy;

}

private BuildTimeSettings createBuildTimeSettings(QuarkusPersistenceUnitDefinition puDefinition,
Expand Down Expand Up @@ -249,7 +252,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 +427,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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
CREATE TABLE known_fruits
(
tenantId VARCHAR(40),
id INT,
name VARCHAR(40)
);
CREATE SEQUENCE known_fruits_id_seq START WITH 7;
INSERT INTO known_fruits(tenantId, id, name) VALUES ('base', 1, 'Cherry');
INSERT INTO known_fruits(tenantId, id, name) VALUES ('base', 2, 'Apple');
INSERT INTO known_fruits(tenantId, id, name) VALUES ('base', 3, 'Banana');

INSERT INTO known_fruits(tenantId, id, name) VALUES ('mycompany', 4, 'Avocado');
INSERT INTO known_fruits(tenantId, id, name) VALUES ('mycompany', 5, 'Apricots');
INSERT INTO known_fruits(tenantId, id, name) VALUES ('mycompany', 6, 'Blackberries');

Loading

0 comments on commit 1bad0d0

Please sign in to comment.