diff --git a/docs/src/main/asciidoc/hibernate-orm.adoc b/docs/src/main/asciidoc/hibernate-orm.adoc index 8a4685b3ce566..421523ec1f7eb 100644 --- a/docs/src/main/asciidoc/hibernate-orm.adoc +++ b/docs/src/main/asciidoc/hibernate-orm.adoc @@ -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. @@ -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. @@ -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, diff --git a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java index 2c994a2cf0f53..cc59601653cd7 100644 --- a/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java +++ b/extensions/hibernate-orm/deployment/src/main/java/io/quarkus/hibernate/orm/deployment/HibernateOrmProcessor.java @@ -1489,11 +1489,6 @@ private static MultiTenancyStrategy getMultiTenancyStrategy(Optional 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; } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/FastBootHibernatePersistenceProvider.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/FastBootHibernatePersistenceProvider.java index 68aa72627142b..5db72232952c5 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/FastBootHibernatePersistenceProvider.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/FastBootHibernatePersistenceProvider.java @@ -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"); diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java index a9ca93cb09317..52db112ee6e5f 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootEntityManagerFactoryBuilder.java @@ -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; @@ -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 @@ -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); } @@ -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)); } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java index 37441b4b287da..85ffce844c7f5 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/boot/FastBootMetadataBuilder.java @@ -112,7 +112,7 @@ public class FastBootMetadataBuilder { private final Collection> additionalIntegrators; private final Collection> providedServices; private final PreGeneratedProxies preGeneratedProxies; - private final Optional dataSource; + private final MultiTenancyStrategy multiTenancyStrategy; private final boolean isReactive; private final boolean fromPersistenceXml; private final List integrationStaticDescriptors; @@ -121,7 +121,6 @@ public class FastBootMetadataBuilder { public FastBootMetadataBuilder(final QuarkusPersistenceUnitDefinition puDefinition, Scanner scanner, Collection> additionalIntegrators, PreGeneratedProxies preGeneratedProxies) { this.persistenceUnit = puDefinition.getActualHibernateDescriptor(); - this.dataSource = puDefinition.getConfig().getDataSource(); this.isReactive = puDefinition.isReactive(); this.fromPersistenceXml = puDefinition.isFromPersistenceXml(); this.additionalIntegrators = additionalIntegrators; @@ -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()); @@ -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, @@ -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. @@ -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); } diff --git a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordedState.java b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordedState.java index c7d9c889df755..d9b728cbea644 100644 --- a/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordedState.java +++ b/extensions/hibernate-orm/runtime/src/main/java/io/quarkus/hibernate/orm/runtime/recording/RecordedState.java @@ -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 { @@ -19,13 +20,15 @@ public final class RecordedState { private final Collection> 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 integrators, Collection> providedServices, IntegrationSettings integrationSettings, - ProxyDefinitions classDefinitions, + ProxyDefinitions classDefinitions, MultiTenancyStrategy strategy, boolean isReactive, boolean fromPersistenceXml) { this.dialect = dialect; this.metadata = metadata; @@ -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; } @@ -66,6 +70,10 @@ public ProxyDefinitions getProxyClassDefinitions() { return proxyClassDefinitions; } + public MultiTenancyStrategy getMultiTenancyStrategy() { + return multiTenancyStrategy; + } + public boolean isReactive() { return isReactive; } diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java index 98a1147dc3f66..3e94c94fd1367 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/FastBootHibernateReactivePersistenceProvider.java @@ -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"); diff --git a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java index 49944951a6a0b..630742c7adc6c 100644 --- a/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java +++ b/extensions/hibernate-reactive/runtime/src/main/java/io/quarkus/hibernate/reactive/runtime/boot/FastBootReactiveEntityManagerFactoryBuilder.java @@ -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 diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/pom.xml b/integration-tests/hibernate-orm-tenancy/discriminator/pom.xml new file mode 100644 index 0000000000000..717941c2d5e1a --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/pom.xml @@ -0,0 +1,181 @@ + + + + + quarkus-integration-test-hibernate-orm-tenancy + io.quarkus + 999-SNAPSHOT + + 4.0.0 + + quarkus-integration-test-hibernate-orm-tenancy-discriminator + Quarkus - Integration Tests - Hibernate - Tenancy - Discriminator + Tests for Hibernate ORM Multitenancy using column discriminator, running with the PostgreSQL database + + + + io.quarkus + quarkus-undertow + + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-jackson + + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + io.quarkus + quarkus-hibernate-orm-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-jdbc-postgresql-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-undertow-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + src/main/resources + true + + + + + maven-surefire-plugin + + true + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + + + + + test-postgresql + + + test-containers + + + + + + maven-surefire-plugin + + false + + + + maven-failsafe-plugin + + false + false + + + + + + + + diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/fruit/CustomTenantResolver.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/fruit/CustomTenantResolver.java new file mode 100644 index 0000000000000..424bb8087c0c3 --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/fruit/CustomTenantResolver.java @@ -0,0 +1,39 @@ +package io.quarkus.it.hibernate.multitenancy.fruit; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; + +import org.jboss.logging.Logger; + +import io.quarkus.hibernate.orm.PersistenceUnitExtension; +import io.quarkus.hibernate.orm.runtime.tenant.TenantResolver; +import io.vertx.ext.web.RoutingContext; + +@PersistenceUnitExtension +@RequestScoped +public class CustomTenantResolver implements TenantResolver { + + private static final Logger LOG = Logger.getLogger(CustomTenantResolver.class); + + @Inject + RoutingContext context; + + @Override + public String getDefaultTenantId() { + return "base"; + } + + @Override + public String resolveTenantId() { + String path = context.request().path(); + final String tenantId; + if (path.startsWith("/mycompany")) { + tenantId = "mycompany"; + } else { + tenantId = getDefaultTenantId(); + } + LOG.debugv("TenantId = {0}", tenantId); + return tenantId; + } + +} diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/fruit/Fruit.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/fruit/Fruit.java new file mode 100644 index 0000000000000..5486526247bac --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/fruit/Fruit.java @@ -0,0 +1,98 @@ +package io.quarkus.it.hibernate.multitenancy.fruit; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import jakarta.xml.bind.annotation.XmlRootElement; + +import org.hibernate.annotations.TenantId; + +@Entity +@Table(name = "known_fruits") +@NamedQuery(name = "Fruits.findAll", query = "SELECT f FROM Fruit f ORDER BY f.name") +@NamedQuery(name = "Fruits.findByName", query = "SELECT f FROM Fruit f WHERE f.name=:name") +@XmlRootElement(name = "fruit") +public class Fruit { + + @TenantId + @Column(length = 40) + private String tenantId; + + @Id + @SequenceGenerator(name = "fruitsSequence", sequenceName = "known_fruits_id_seq", allocationSize = 1, initialValue = 10) + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "fruitsSequence") + private Integer id; + + @Column(length = 40, unique = true) + private String name; + + public Fruit() { + } + + public Fruit(String name) { + this.name = name; + } + + public Fruit(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Fruit other = (Fruit) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + @Override + public String toString() { + return "Fruit [id=" + id + ", name=" + name + "]"; + } + +} diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/fruit/FruitResource.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/fruit/FruitResource.java new file mode 100644 index 0000000000000..44fb2aba771b0 --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/fruit/FruitResource.java @@ -0,0 +1,207 @@ +package io.quarkus.it.hibernate.multitenancy.fruit; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.PUT; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.jaxrs.PathParam; + +@ApplicationScoped +@Produces("application/json") +@Consumes("application/json") +@Path("/") +public class FruitResource { + + private static final Logger LOG = Logger.getLogger(FruitResource.class.getName()); + + @Inject + EntityManager entityManager; + + @GET + @Path("fruits") + public Fruit[] getDefault() { + return get(); + } + + @GET + @Path("{tenant}/fruits") + public Fruit[] getTenant() { + return get(); + } + + private Fruit[] get() { + return entityManager.createNamedQuery("Fruits.findAll", Fruit.class) + .getResultList().toArray(new Fruit[0]); + } + + @GET + @Path("fruits/{id}") + public Fruit getSingleDefault(@PathParam("id") int id) { + return findById(id); + } + + @GET + @Path("{tenant}/fruits/{id}") + public Fruit getSingleTenant(@PathParam("id") int id) { + return findById(id); + } + + private Fruit findById(int id) { + Fruit entity = entityManager.find(Fruit.class, id); + if (entity == null) { + throw new WebApplicationException("Fruit with id of " + id + " does not exist.", 404); + } + return entity; + } + + @POST + @Transactional + @Path("fruits") + public Response createDefault(@NotNull Fruit fruit) { + return create(fruit); + } + + @POST + @Transactional + @Path("{tenant}/fruits") + public Response createTenant(@NotNull Fruit fruit) { + return create(fruit); + } + + private Response create(@NotNull Fruit fruit) { + if (fruit.getId() != null) { + throw new WebApplicationException("Id was invalidly set on request.", 422); + } + LOG.debugv("Create {0}", fruit.getName()); + entityManager.persist(fruit); + return Response.ok(fruit).status(201).build(); + } + + @PUT + @Path("fruits/{id}") + @Transactional + public Fruit updateDefault(@PathParam("id") int id, @NotNull Fruit fruit) { + return update(id, fruit); + } + + @PUT + @Path("{tenant}/fruits/{id}") + @Transactional + public Fruit updateTenant(@PathParam("id") int id, @NotNull Fruit fruit) { + return update(id, fruit); + } + + private Fruit update(@NotNull @PathParam("id") int id, @NotNull Fruit fruit) { + if (fruit.getName() == null) { + throw new WebApplicationException("Fruit Name was not set on request.", 422); + } + + Fruit entity = entityManager.find(Fruit.class, id); + if (entity == null) { + throw new WebApplicationException("Fruit with id of " + id + " does not exist.", 404); + } + entity.setName(fruit.getName()); + + LOG.debugv("Update #{0} {1}", fruit.getId(), fruit.getName()); + + return entity; + } + + @DELETE + @Path("fruits/{id}") + @Transactional + public Response deleteDefault(@PathParam("id") int id) { + return delete(id); + } + + @DELETE + @Path("{tenant}/fruits/{id}") + @Transactional + public Response deleteTenant(@PathParam("id") int id) { + return delete(id); + } + + private Response delete(int id) { + Fruit fruit = entityManager.getReference(Fruit.class, id); + if (fruit == null) { + throw new WebApplicationException("Fruit with id of " + id + " does not exist.", 404); + } + LOG.debugv("Delete #{0} {1}", fruit.getId(), fruit.getName()); + entityManager.remove(fruit); + return Response.status(204).build(); + } + + @GET + @Path("fruitsFindBy") + public Response findByDefault(@NotNull @QueryParam("type") String type, @NotNull @QueryParam("value") String value) { + return findBy(type, value); + } + + @GET + @Path("{tenant}/fruitsFindBy") + public Response findByTenant(@NotNull @QueryParam("type") String type, @NotNull @QueryParam("value") String value) { + return findBy(type, value); + } + + private Response findBy(@NotNull String type, @NotNull String value) { + if (!"name".equalsIgnoreCase(type)) { + throw new IllegalArgumentException("Currently only 'fruitsFindBy?type=name' is supported"); + } + List list = entityManager.createNamedQuery("Fruits.findByName", Fruit.class).setParameter("name", value) + .getResultList(); + if (list.size() == 0) { + return Response.status(404).build(); + } + Fruit fruit = list.get(0); + return Response.status(200).entity(fruit).build(); + } + + @Provider + public static class ErrorMapper implements ExceptionMapper { + + @Override + public Response toResponse(Exception exception) { + LOG.error("Failed to handle request", exception); + + int code = 500; + if (exception instanceof WebApplicationException) { + code = ((WebApplicationException) exception).getResponse().getStatus(); + } + + Error error = new Error(); + error.exceptionType = exception.getClass().getName(); + error.code = code; + error.error = exception.getMessage(); + + return Response.status(code) + .type(MediaType.APPLICATION_JSON) + .entity(error) + .build(); + } + } + + public static class Error { + + public String exceptionType; + public int code; + public String error; + } +} diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/inventory/InventoryResource.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/inventory/InventoryResource.java new file mode 100644 index 0000000000000..cc87aa657e18b --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/inventory/InventoryResource.java @@ -0,0 +1,39 @@ +package io.quarkus.it.hibernate.multitenancy.inventory; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; + +import io.quarkus.hibernate.orm.PersistenceUnit; + +@ApplicationScoped +@Produces("application/json") +@Consumes("application/json") +@Path("/") +public class InventoryResource { + + @Inject + @PersistenceUnit("inventory") + EntityManager entityManager; + + @GET + @Path("inventory") + public Plane[] getDefault() { + return get(); + } + + @GET + @Path("{tenant}/inventory") + public Plane[] getTenant() { + return get(); + } + + private Plane[] get() { + return entityManager.createNamedQuery("Planes.findAll", Plane.class) + .getResultList().toArray(new Plane[0]); + } +} diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/inventory/InventoryTenantResolver.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/inventory/InventoryTenantResolver.java new file mode 100644 index 0000000000000..3dc77844c3102 --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/inventory/InventoryTenantResolver.java @@ -0,0 +1,39 @@ +package io.quarkus.it.hibernate.multitenancy.inventory; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; + +import org.jboss.logging.Logger; + +import io.quarkus.hibernate.orm.PersistenceUnitExtension; +import io.quarkus.hibernate.orm.runtime.tenant.TenantResolver; +import io.vertx.ext.web.RoutingContext; + +@PersistenceUnitExtension("inventory") +@RequestScoped +public class InventoryTenantResolver implements TenantResolver { + + private static final Logger LOG = Logger.getLogger(InventoryTenantResolver.class); + + @Inject + RoutingContext context; + + @Override + public String getDefaultTenantId() { + return "inventory"; + } + + @Override + public String resolveTenantId() { + String path = context.request().path(); + final String tenantId; + if (path.startsWith("/mycompany")) { + tenantId = "inventorymycompany"; + } else { + tenantId = getDefaultTenantId(); + } + LOG.debugv("TenantId = {0}", tenantId); + return tenantId; + } + +} diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/inventory/Plane.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/inventory/Plane.java new file mode 100644 index 0000000000000..ddf384c73f024 --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/java/io/quarkus/it/hibernate/multitenancy/inventory/Plane.java @@ -0,0 +1,74 @@ +package io.quarkus.it.hibernate.multitenancy.inventory; + +import java.util.Objects; + +import jakarta.persistence.*; + +import org.hibernate.annotations.TenantId; + +@Entity +@Table(name = "plane") +@NamedQuery(name = "Planes.findAll", query = "SELECT p FROM Plane p ORDER BY p.name") +public class Plane { + + @TenantId + @Column(length = 40) + private String tenantId; + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "plane_iq_seq") + private Long id; + + @Column(length = 40, unique = true) + private String name; + + public Plane() { + } + + public Plane(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Plane other = (Plane) obj; + return Objects.equals(this.name, other.name) + && Objects.equals(this.id, other.id); + } + + @Override + public String toString() { + return "Plane [id=" + id + ", name=" + name + "]"; + } +} diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/main/resources/application.properties b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/resources/application.properties new file mode 100644 index 0000000000000..b01c631e80f96 --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/resources/application.properties @@ -0,0 +1,14 @@ +# Database configuration +quarkus.datasource.db-kind=postgresql + +# Default persistence unit +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.multitenant=discriminator +quarkus.hibernate-orm.packages=io.quarkus.it.hibernate.multitenancy.fruit + +# Inventory persistence unit +quarkus.hibernate-orm."inventory".database.generation=drop-and-create +quarkus.hibernate-orm."inventory".multitenant=discriminator +quarkus.hibernate-orm."inventory".datasource= +quarkus.hibernate-orm."inventory".packages=io.quarkus.it.hibernate.multitenancy.inventory +quarkus.hibernate-orm."inventory".sql-load-script=import-inventory.sql diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/main/resources/import-inventory.sql b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/resources/import-inventory.sql new file mode 100644 index 0000000000000..e3fe72a3ba6ea --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/resources/import-inventory.sql @@ -0,0 +1,5 @@ +INSERT INTO plane(tenantId, id, name) VALUES ('inventory', 1, 'Airbus A320'); +INSERT INTO plane(tenantId, id, name) VALUES ('inventory', 2, 'Airbus A350'); + +INSERT INTO plane(tenantId, id, name) VALUES ('inventorymycompany', 3, 'Boeing 737'); +INSERT INTO plane(tenantId, id, name) VALUES ('inventorymycompany', 4, 'Boeing 747'); \ No newline at end of file diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/main/resources/import.sql b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/resources/import.sql new file mode 100644 index 0000000000000..ea0527f2bdbda --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/main/resources/import.sql @@ -0,0 +1,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'); diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/fruit/HibernateTenancyFunctionalityInGraalITCase.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/fruit/HibernateTenancyFunctionalityInGraalITCase.java new file mode 100644 index 0000000000000..d9ae0c86a83ea --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/fruit/HibernateTenancyFunctionalityInGraalITCase.java @@ -0,0 +1,11 @@ +package io.quarkus.it.hibernate.multitenancy.fruit; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +/** + * Test various JPA operations running in native mode + */ +@QuarkusIntegrationTest +public class HibernateTenancyFunctionalityInGraalITCase extends HibernateTenancyFunctionalityTest { + +} diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/fruit/HibernateTenancyFunctionalityTest.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/fruit/HibernateTenancyFunctionalityTest.java new file mode 100644 index 0000000000000..6d3090f0d0208 --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/fruit/HibernateTenancyFunctionalityTest.java @@ -0,0 +1,167 @@ +package io.quarkus.it.hibernate.multitenancy.fruit; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +import jakarta.ws.rs.core.Response.Status; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import io.restassured.response.Response; + +/** + * Test various Hibernate Multitenancy operations running in Quarkus + */ +@QuarkusTest +public class HibernateTenancyFunctionalityTest { + + @BeforeEach + public void cleanup() { + + deleteIfExists("", "Dragonfruit"); + deleteIfExists("", "Gooseberry"); + deleteIfExists("/mycompany", "Damson"); + deleteIfExists("/mycompany", "Grapefruit"); + + } + + @Test + public void testAddDeleteDefaultTenant() throws Exception { + + // Create fruit for default 'base' tenant + given().with().body(new Fruit("Delete")).contentType(ContentType.JSON).when().post("/fruits").then() + .assertThat().statusCode(is(Status.CREATED.getStatusCode())); + + // Get it + Fruit newFruit = findByName("", "Delete"); + + // Delete it + given().pathParam("id", newFruit.getId()).contentType("application/json").accept("application/json") + .when().delete("/fruits/{id}").then().assertThat().statusCode(is(Status.NO_CONTENT.getStatusCode())); + + } + + @Test + public void testGetFruitsDefaultTenant() throws Exception { + + Fruit[] fruits = given().when().get("/fruits").then().assertThat() + .statusCode(is(Status.OK.getStatusCode())).extract() + .as(Fruit[].class); + assertThat(fruits, arrayContaining(new Fruit(2, "Apple"), new Fruit(3, "Banana"), new Fruit(1, "Cherry"))); + + } + + @Test + public void testGetFruitsTenantMycompany() throws Exception { + + Fruit[] fruits = given().when().get("/mycompany/fruits").then().assertThat() + .statusCode(is(Status.OK.getStatusCode())).extract() + .as(Fruit[].class); + assertThat(fruits, arrayWithSize(3)); + assertThat(fruits, arrayContaining(new Fruit(5, "Apricots"), new Fruit(4, "Avocado"), new Fruit(6, "Blackberries"))); + + } + + @Test + public void testPostFruitDefaultTenant() throws Exception { + + // Create fruit for default 'base' tenant + Fruit newFruit = new Fruit("Dragonfruit"); + given().with().body(newFruit).contentType(ContentType.JSON).when().post("/fruits").then() + .assertThat() + .statusCode(is(Status.CREATED.getStatusCode())); + + // Getting it directly should return the new fruit + Fruit dragonFruit = findByName("", newFruit.getName()); + assertThat(dragonFruit, not(is(nullValue()))); + + // Getting fruit list should also contain the new fruit + Fruit[] baseFruits = given().when().get("/fruits").then().assertThat() + .statusCode(is(Status.OK.getStatusCode())).extract() + .as(Fruit[].class); + assertThat(baseFruits, arrayWithSize(4)); + assertThat(baseFruits, + arrayContaining(new Fruit(2, "Apple"), new Fruit(3, "Banana"), new Fruit(1, "Cherry"), dragonFruit)); + + // The other tenant should NOT have the new fruit + Fruit[] mycompanyFruits = given().when().get("/mycompany/fruits").then().assertThat() + .statusCode(is(Status.OK.getStatusCode())) + .extract().as(Fruit[].class); + assertThat(mycompanyFruits, arrayWithSize(3)); + assertThat(mycompanyFruits, + arrayContaining(new Fruit(5, "Apricots"), new Fruit(4, "Avocado"), new Fruit(6, "Blackberries"))); + + // Getting it directly should also NOT return the new fruit + assertThat(findByName("/mycompany", newFruit.getName()), is(nullValue())); + + } + + @Test + public void testUpdateFruitDefaultTenant() throws Exception { + + // Create fruits for both tenants + + Fruit newFruitBase = new Fruit("Dragonfruit"); + given().with().body(newFruitBase).contentType(ContentType.JSON).when().post("/fruits").then() + .assertThat() + .statusCode(is(Status.CREATED.getStatusCode())); + Fruit baseFruit = findByName("", newFruitBase.getName()); + assertThat(baseFruit, not(is(nullValue()))); + + Fruit newFruitMycompany = new Fruit("Damson"); + given().with().body(newFruitMycompany).contentType(ContentType.JSON).when().post("/mycompany/fruits") + .then().assertThat() + .statusCode(is(Status.CREATED.getStatusCode())); + Fruit mycompanyFruit = findByName("/mycompany", newFruitMycompany.getName()); + assertThat(mycompanyFruit, not(is(nullValue()))); + + // Update both + + String baseFruitName = "Gooseberry"; + baseFruit.setName(baseFruitName); + given().with().body(baseFruit).contentType(ContentType.JSON).when() + .put("/fruits/{id}", baseFruit.getId()).then().assertThat() + .statusCode(is(Status.OK.getStatusCode())); + + String mycompanyFruitName = "Grapefruit"; + mycompanyFruit.setName(mycompanyFruitName); + given().with().body(mycompanyFruit).contentType(ContentType.JSON).when() + .put("/mycompany/fruits/{id}", mycompanyFruit.getId()) + .then().assertThat().statusCode(is(Status.OK.getStatusCode())); + + // Check if we can get them back and they only exist for one tenant + + assertThat(findByName("", baseFruitName), is(not(nullValue()))); + assertThat(findByName("/mycompany", baseFruitName), is(nullValue())); + + assertThat(findByName("/mycompany", mycompanyFruitName), is(not(nullValue()))); + assertThat(findByName("", mycompanyFruitName), is(nullValue())); + + } + + private Fruit findByName(String tenantPath, String name) { + Response response = given().when().get(tenantPath + "/fruitsFindBy?type=name&value={name}", name); + if (response.getStatusCode() == Status.OK.getStatusCode()) { + return response.as(Fruit.class); + } + return null; + } + + private void deleteIfExists(String tenantPath, String name) { + Fruit dragonFruit = findByName(tenantPath, name); + if (dragonFruit != null) { + given().pathParam("id", dragonFruit.getId()).when().delete(tenantPath + "/fruits/{id}").then() + .assertThat() + .statusCode(is(Status.NO_CONTENT.getStatusCode())); + } + } + +} diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/inventory/HibernateNamedPersistenceUnitTest.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/inventory/HibernateNamedPersistenceUnitTest.java new file mode 100644 index 0000000000000..0bc439eadd99c --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/inventory/HibernateNamedPersistenceUnitTest.java @@ -0,0 +1,36 @@ +package io.quarkus.it.hibernate.multitenancy.inventory; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.arrayContaining; +import static org.hamcrest.Matchers.is; + +import jakarta.ws.rs.core.Response.Status; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class HibernateNamedPersistenceUnitTest { + + @Test + public void testGetPlanesDefaultTenant() throws Exception { + + Plane[] planes = given().when().get("/inventory").then().assertThat() + .statusCode(is(Status.OK.getStatusCode())).extract() + .as(Plane[].class); + assertThat(planes, arrayContaining(new Plane(1L, "Airbus A320"), new Plane(2L, "Airbus A350"))); + + } + + @Test + public void testGetPlanesTenantMycompany() throws Exception { + + Plane[] planes = given().when().get("/mycompany/inventory").then().assertThat() + .statusCode(is(Status.OK.getStatusCode())).extract() + .as(Plane[].class); + assertThat(planes, arrayContaining(new Plane(3L, "Boeing 737"), new Plane(4L, "Boeing 747"))); + + } +} diff --git a/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/inventory/HibernateNamedPersistenceUnitTestInGraalITCase.java b/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/inventory/HibernateNamedPersistenceUnitTestInGraalITCase.java new file mode 100644 index 0000000000000..79ba8001baed8 --- /dev/null +++ b/integration-tests/hibernate-orm-tenancy/discriminator/src/test/java/io/quarkus/it/hibernate/multitenancy/inventory/HibernateNamedPersistenceUnitTestInGraalITCase.java @@ -0,0 +1,8 @@ +package io.quarkus.it.hibernate.multitenancy.inventory; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +public class HibernateNamedPersistenceUnitTestInGraalITCase extends HibernateNamedPersistenceUnitTest { + +} diff --git a/integration-tests/hibernate-orm-tenancy/pom.xml b/integration-tests/hibernate-orm-tenancy/pom.xml index 08cfae88cf0b9..c88c3bd3628d9 100644 --- a/integration-tests/hibernate-orm-tenancy/pom.xml +++ b/integration-tests/hibernate-orm-tenancy/pom.xml @@ -17,6 +17,7 @@ datasource connection-resolver schema + discriminator connection-resolver-legacy-qualifiers