diff --git a/jpa/multitenant/.gitignore b/jpa/multitenant/.gitignore new file mode 100644 index 00000000..549e00a2 --- /dev/null +++ b/jpa/multitenant/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/jpa/multitenant/README.adoc b/jpa/multitenant/README.adoc new file mode 100644 index 00000000..0c751149 --- /dev/null +++ b/jpa/multitenant/README.adoc @@ -0,0 +1,10 @@ +This is the parent project for a couple of examples demonstrating how to integrate Hibernates Multitenant feature with Spring Data JPA. + +There are three modules for the three examples. + +Each uses a different strategy to separate data by tenant: + +1. Partition tables by tenant id. +2. Use a separate schema per tenant +3. Use a separate database per tenant. + diff --git a/jpa/multitenant/db/pom.xml b/jpa/multitenant/db/pom.xml new file mode 100644 index 00000000..60c70aa4 --- /dev/null +++ b/jpa/multitenant/db/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + hibernate-multitenant-db + + + org.springframework.data.examples + spring-data-jpa-hibernate-multitenant-examples + 2.0.0.BUILD-SNAPSHOT + + + Hibernate Multitenant DB + + Example project demonstrating the integration of Hibernates Multitenant feature with Spring Boot using separate databases. + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/Application.java b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/Application.java new file mode 100644 index 00000000..e3863b9d --- /dev/null +++ b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/Application.java @@ -0,0 +1,11 @@ +package example.springdata.jpa.hibernatemultitenant.db; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/NoOpConnectionProvider.java b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/NoOpConnectionProvider.java new file mode 100644 index 00000000..7e3a48b0 --- /dev/null +++ b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/NoOpConnectionProvider.java @@ -0,0 +1,75 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.db; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.stereotype.Component; + +@Component +public class NoOpConnectionProvider implements MultiTenantConnectionProvider, HibernatePropertiesCustomizer { + + @Autowired DataSource dataSource; + + @Override + public Connection getAnyConnection() throws SQLException { + return dataSource.getConnection(); + } + + @Override + public void releaseAnyConnection(Connection connection) throws SQLException { + connection.close(); + } + + @Override + public Connection getConnection(String schema) throws SQLException { + + return dataSource.getConnection(); + } + + @Override + public void releaseConnection(String s, Connection connection) throws SQLException { + connection.close(); + } + + @Override + public boolean supportsAggressiveRelease() { + return false; + } + + @Override + public boolean isUnwrappableAs(Class aClass) { + return false; + } + + @Override + public T unwrap(Class aClass) { + throw new UnsupportedOperationException("Can't unwrap this."); + } + + @Override + public void customize(Map hibernateProperties) { + hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, this); + } +} diff --git a/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/Person.java b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/Person.java new file mode 100644 index 00000000..00215f99 --- /dev/null +++ b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/Person.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.db; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Person { + + @Id @GeneratedValue private Long id; + + private String 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 String toString() { + return "Person{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} diff --git a/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/Persons.java b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/Persons.java new file mode 100644 index 00000000..5cface36 --- /dev/null +++ b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/Persons.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.db; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface Persons extends JpaRepository { + + static Person named(String name) { + + Person person = new Person(); + person.setName(name); + return person; + } +} diff --git a/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/TenantIdentifierResolver.java b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/TenantIdentifierResolver.java new file mode 100644 index 00000000..3efb038f --- /dev/null +++ b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/TenantIdentifierResolver.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.db; + +import java.util.Map; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.stereotype.Component; + +@Component() +public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver, HibernatePropertiesCustomizer { + + private String currentTenant = "unknown"; + + public void setCurrentTenant(String tenant) { + currentTenant = tenant; + } + + @Override + public String resolveCurrentTenantIdentifier() { + return currentTenant; + } + + @Override + public boolean validateExistingCurrentSessions() { + return false; + } + + @Override + public void customize(Map hibernateProperties) { + hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this); + } +} diff --git a/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/TenantRoutingDatasource.java b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/TenantRoutingDatasource.java new file mode 100644 index 00000000..ed4438d4 --- /dev/null +++ b/jpa/multitenant/db/src/main/java/example/springdata/jpa/hibernatemultitenant/db/TenantRoutingDatasource.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.db; + +import java.util.HashMap; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.stereotype.Component; + +@Component +public class TenantRoutingDatasource extends AbstractRoutingDataSource { + + @Autowired private TenantIdentifierResolver tenantIdentifierResolver; + + TenantRoutingDatasource() { + + setDefaultTargetDataSource(createEmbeddedDatabase("default")); + + HashMap targetDataSources = new HashMap<>(); + targetDataSources.put("VMWARE", createEmbeddedDatabase("VMWARE")); + targetDataSources.put("PIVOTAL", createEmbeddedDatabase("PIVOTAL")); + setTargetDataSources(targetDataSources); + } + + @Override + protected String determineCurrentLookupKey() { + return tenantIdentifierResolver.resolveCurrentTenantIdentifier(); + } + + private EmbeddedDatabase createEmbeddedDatabase(String name) { + + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).setName(name).addScript("manual-schema.sql") + .build(); + } +} diff --git a/jpa/multitenant/db/src/main/resources/application.properties b/jpa/multitenant/db/src/main/resources/application.properties new file mode 100644 index 00000000..e69de29b diff --git a/jpa/multitenant/db/src/main/resources/manual-schema.sql b/jpa/multitenant/db/src/main/resources/manual-schema.sql new file mode 100644 index 00000000..aca3da5c --- /dev/null +++ b/jpa/multitenant/db/src/main/resources/manual-schema.sql @@ -0,0 +1,2 @@ +create sequence person_seq start with 1 increment by 50; +create table person (id bigint not null, name varchar(255), primary key (id)); diff --git a/jpa/multitenant/db/src/test/java/example/springdata/jpa/hibernatemultitenant/db/ApplicationTests.java b/jpa/multitenant/db/src/test/java/example/springdata/jpa/hibernatemultitenant/db/ApplicationTests.java new file mode 100644 index 00000000..3e4a1878 --- /dev/null +++ b/jpa/multitenant/db/src/test/java/example/springdata/jpa/hibernatemultitenant/db/ApplicationTests.java @@ -0,0 +1,49 @@ +package example.springdata.jpa.hibernatemultitenant.db; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.transaction.support.TransactionTemplate; + +@SpringBootTest +@TestExecutionListeners(listeners = { DependencyInjectionTestExecutionListener.class }) +class ApplicationTests { + + public static final String PIVOTAL = "PIVOTAL"; + public static final String VMWARE = "VMWARE"; + @Autowired Persons persons; + + @Autowired TransactionTemplate txTemplate; + + @Autowired TenantIdentifierResolver currentTenant; + + @Test + void saveAndLoadPerson() { + + createPerson(PIVOTAL, "Adam"); + createPerson(VMWARE, "Eve"); + + currentTenant.setCurrentTenant(VMWARE); + assertThat(persons.findAll()).extracting(Person::getName).containsExactly("Eve"); + + currentTenant.setCurrentTenant(PIVOTAL); + assertThat(persons.findAll()).extracting(Person::getName).containsExactly("Adam"); + } + + private Person createPerson(String schema, String name) { + + currentTenant.setCurrentTenant(schema); + + Person adam = txTemplate.execute(tx -> { + Person person = Persons.named(name); + return persons.save(person); + }); + + assertThat(adam.getId()).isNotNull(); + return adam; + } +} diff --git a/jpa/multitenant/partition/pom.xml b/jpa/multitenant/partition/pom.xml new file mode 100644 index 00000000..bac57881 --- /dev/null +++ b/jpa/multitenant/partition/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + hibernate-multitenant-partition + + + org.springframework.data.examples + spring-data-jpa-hibernate-multitenant-examples + 2.0.0.BUILD-SNAPSHOT + + + Hibernate Multitenant Partition + + Example project demonstrating the integration of Hibernates Multitenant feature with Spring Boot using partitioned tables. + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/Application.java b/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/Application.java new file mode 100644 index 00000000..0622c108 --- /dev/null +++ b/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/Application.java @@ -0,0 +1,11 @@ +package example.springdata.jpa.hibernatemultitenant.partition; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/Person.java b/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/Person.java new file mode 100644 index 00000000..1ea11d11 --- /dev/null +++ b/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/Person.java @@ -0,0 +1,61 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.partition; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.annotations.TenantId; + +@Entity +public class Person { + + @Id @GeneratedValue private Long id; + + @TenantId private String tenant; + + private String 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; + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + @Override + public String toString() { + return "Person{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} diff --git a/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/Persons.java b/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/Persons.java new file mode 100644 index 00000000..d60c309b --- /dev/null +++ b/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/Persons.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.partition; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +public interface Persons extends JpaRepository { + + static Person named(String name) { + Person person = new Person(); + person.setName(name); + return person; + } + + @Query("select p from Person p where name = :name") + Person findJpqlByName(String name); + + @Query(value = "select * from Person p where name = :name", nativeQuery = true) + Person findSqlByName(String name); +} diff --git a/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/TenantIdentifierResolver.java b/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/TenantIdentifierResolver.java new file mode 100644 index 00000000..15d40257 --- /dev/null +++ b/jpa/multitenant/partition/src/main/java/example/springdata/jpa/hibernatemultitenant/partition/TenantIdentifierResolver.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.partition; + +import java.util.Map; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.stereotype.Component; + +@Component() +public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver, HibernatePropertiesCustomizer { + + private String currentTenant = "unknown"; + + public void setCurrentTenant(String tenant) { + currentTenant = tenant; + } + + @Override + public String resolveCurrentTenantIdentifier() { + return currentTenant; + } + + @Override + public boolean validateExistingCurrentSessions() { + return false; + } + + @Override + public void customize(Map hibernateProperties) { + hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this); + } +} diff --git a/jpa/multitenant/partition/src/main/resources/application.properties b/jpa/multitenant/partition/src/main/resources/application.properties new file mode 100644 index 00000000..e69de29b diff --git a/jpa/multitenant/partition/src/main/resources/schema.sql b/jpa/multitenant/partition/src/main/resources/schema.sql new file mode 100644 index 00000000..e5fbfb11 --- /dev/null +++ b/jpa/multitenant/partition/src/main/resources/schema.sql @@ -0,0 +1,2 @@ +create sequence person_seq start with 1 increment by 50; +create table person (id bigint not null, name varchar(255), tenant varchar(255) not null, primary key (id)); diff --git a/jpa/multitenant/partition/src/test/java/example/springdata/jpa/hibernatemultitenant/partition/ApplicationTests.java b/jpa/multitenant/partition/src/test/java/example/springdata/jpa/hibernatemultitenant/partition/ApplicationTests.java new file mode 100644 index 00000000..e953edf2 --- /dev/null +++ b/jpa/multitenant/partition/src/test/java/example/springdata/jpa/hibernatemultitenant/partition/ApplicationTests.java @@ -0,0 +1,97 @@ +package example.springdata.jpa.hibernatemultitenant.partition; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.transaction.support.TransactionTemplate; + +@SpringBootTest +@TestExecutionListeners(listeners = { DependencyInjectionTestExecutionListener.class }) +class ApplicationTests { + + public static final String PIVOTAL = "PIVOTAL"; + public static final String VMWARE = "VMWARE"; + @Autowired Persons persons; + + @Autowired TransactionTemplate txTemplate; + + @Autowired TenantIdentifierResolver currentTenant; + + @AfterEach + void afterEach() { + currentTenant.setCurrentTenant(VMWARE); + persons.deleteAll(); + currentTenant.setCurrentTenant(PIVOTAL); + persons.deleteAll(); + } + + @Test + void saveAndLoadPerson() { + + final Person adam = createPerson(PIVOTAL, "Adam"); + final Person eve = createPerson(VMWARE, "Eve"); + + assertThat(adam.getTenant()).isEqualTo(PIVOTAL); + assertThat(eve.getTenant()).isEqualTo(VMWARE); + + currentTenant.setCurrentTenant(VMWARE); + assertThat(persons.findAll()).extracting(Person::getName).containsExactly("Eve"); + + currentTenant.setCurrentTenant(PIVOTAL); + assertThat(persons.findAll()).extracting(Person::getName).containsExactly("Adam"); + } + + @Test + void findById() { + + final Person adam = createPerson(PIVOTAL, "Adam"); + final Person vAdam = createPerson(VMWARE, "Adam"); + + currentTenant.setCurrentTenant(VMWARE); + assertThat(persons.findById(vAdam.getId()).get().getTenant()).isEqualTo(VMWARE); + assertThat(persons.findById(adam.getId())).isEmpty(); + } + + @Test + void queryJPQL() { + + createPerson(PIVOTAL, "Adam"); + createPerson(VMWARE, "Adam"); + createPerson(VMWARE, "Eve"); + + currentTenant.setCurrentTenant(VMWARE); + assertThat(persons.findJpqlByName("Adam").getTenant()).isEqualTo(VMWARE); + + currentTenant.setCurrentTenant(PIVOTAL); + assertThat(persons.findJpqlByName("Eve")).isNull(); + } + + @Test + void querySQL() { + + createPerson(PIVOTAL, "Adam"); + createPerson(VMWARE, "Adam"); + + currentTenant.setCurrentTenant(VMWARE); + assertThatThrownBy(() -> persons.findSqlByName("Adam")).isInstanceOf(IncorrectResultSizeDataAccessException.class); + } + + private Person createPerson(String schema, String name) { + + currentTenant.setCurrentTenant(schema); + + Person adam = txTemplate.execute(tx -> { + Person person = Persons.named(name); + return persons.save(person); + }); + + assertThat(adam.getId()).isNotNull(); + return adam; + } +} diff --git a/jpa/multitenant/pom.xml b/jpa/multitenant/pom.xml new file mode 100644 index 00000000..0349f238 --- /dev/null +++ b/jpa/multitenant/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + org.springframework.data.examples + spring-data-jpa-hibernate-multitenant-examples + 2.0.0.BUILD-SNAPSHOT + + pom + + + org.springframework.boot + spring-boot-starter-parent + 3.0.0-M4 + + + Hibernate Multitenant Examples + + A set of projects demonstrating how Hibernates Multitenant feature can be integrated with Spring Boot and Spring Data JPA. + + + + partition + schema + db + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + + diff --git a/jpa/multitenant/schema/pom.xml b/jpa/multitenant/schema/pom.xml new file mode 100644 index 00000000..889b4a10 --- /dev/null +++ b/jpa/multitenant/schema/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + hibernate-multitenant-schema + + + org.springframework.data.examples + spring-data-jpa-hibernate-multitenant-examples + 2.0.0.BUILD-SNAPSHOT + + + Hibernate Multitenant Schema + + Example project demonstrating the integration of Hibernates Multitenant feature with Spring Boot using different schema. + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/Application.java b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/Application.java new file mode 100644 index 00000000..1366f1ab --- /dev/null +++ b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/Application.java @@ -0,0 +1,11 @@ +package example.springdata.jpa.hibernatemultitenant.schema; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/ExampleConnectionProvider.java b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/ExampleConnectionProvider.java new file mode 100644 index 00000000..c482c5f8 --- /dev/null +++ b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/ExampleConnectionProvider.java @@ -0,0 +1,77 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.schema; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Map; + +import javax.sql.DataSource; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.stereotype.Component; + +@Component +public class ExampleConnectionProvider implements MultiTenantConnectionProvider, HibernatePropertiesCustomizer { + + @Autowired DataSource dataSource; + + @Override + public Connection getAnyConnection() throws SQLException { + return getConnection("PUBLIC"); + } + + @Override + public void releaseAnyConnection(Connection connection) throws SQLException { + connection.close(); + } + + @Override + public Connection getConnection(String schema) throws SQLException { + final Connection connection = dataSource.getConnection(); + connection.setSchema(schema); + return connection; + } + + @Override + public void releaseConnection(String s, Connection connection) throws SQLException { + connection.setSchema("PUBLIC"); + connection.close(); + } + + @Override + public boolean supportsAggressiveRelease() { + return false; + } + + @Override + public boolean isUnwrappableAs(Class aClass) { + return false; + } + + @Override + public T unwrap(Class aClass) { + throw new UnsupportedOperationException("Can't unwrap this."); + } + + @Override + public void customize(Map hibernateProperties) { + hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, this); + } +} diff --git a/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/Person.java b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/Person.java new file mode 100644 index 00000000..2fecec6b --- /dev/null +++ b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/Person.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.schema; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Person { + + @Id @GeneratedValue private Long id; + + private String 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 String toString() { + return "Person{" + "id=" + id + ", name='" + name + '\'' + '}'; + } +} diff --git a/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/Persons.java b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/Persons.java new file mode 100644 index 00000000..91e64003 --- /dev/null +++ b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/Persons.java @@ -0,0 +1,26 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.schema; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface Persons extends JpaRepository { + static Person named(String name) { + Person person = new Person(); + person.setName(name); + return person; + } +} diff --git a/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/TenantIdentifierResolver.java b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/TenantIdentifierResolver.java new file mode 100644 index 00000000..c117a6ef --- /dev/null +++ b/jpa/multitenant/schema/src/main/java/example/springdata/jpa/hibernatemultitenant/schema/TenantIdentifierResolver.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.springdata.jpa.hibernatemultitenant.schema; + +import java.util.Map; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer; +import org.springframework.stereotype.Component; + +@Component() +public class TenantIdentifierResolver implements CurrentTenantIdentifierResolver, HibernatePropertiesCustomizer { + + private String currentTenant = "unknown"; + + public void setCurrentTenant(String tenant) { + currentTenant = tenant; + } + + @Override + public String resolveCurrentTenantIdentifier() { + return currentTenant; + } + + @Override + public boolean validateExistingCurrentSessions() { + return false; + } + + @Override + public void customize(Map hibernateProperties) { + hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this); + } +} diff --git a/jpa/multitenant/schema/src/main/resources/application.properties b/jpa/multitenant/schema/src/main/resources/application.properties new file mode 100644 index 00000000..e69de29b diff --git a/jpa/multitenant/schema/src/main/resources/schema.sql b/jpa/multitenant/schema/src/main/resources/schema.sql new file mode 100644 index 00000000..46b30aa7 --- /dev/null +++ b/jpa/multitenant/schema/src/main/resources/schema.sql @@ -0,0 +1,8 @@ +create schema if not exists pivotal; +create schema if not exists vmware; + +create sequence pivotal.person_seq start with 1 increment by 50; +create table pivotal.person (id bigint not null, name varchar(255), primary key (id)); + +create sequence vmware.person_seq start with 1 increment by 50; +create table vmware.person (id bigint not null, name varchar(255), primary key (id)); diff --git a/jpa/multitenant/schema/src/test/java/example/springdata/jpa/hibernatemultitenant/schema/ApplicationTests.java b/jpa/multitenant/schema/src/test/java/example/springdata/jpa/hibernatemultitenant/schema/ApplicationTests.java new file mode 100644 index 00000000..c627eca7 --- /dev/null +++ b/jpa/multitenant/schema/src/test/java/example/springdata/jpa/hibernatemultitenant/schema/ApplicationTests.java @@ -0,0 +1,49 @@ +package example.springdata.jpa.hibernatemultitenant.schema; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.transaction.support.TransactionTemplate; + +@SpringBootTest +@TestExecutionListeners(listeners = { DependencyInjectionTestExecutionListener.class }) +class ApplicationTests { + + public static final String PIVOTAL = "PIVOTAL"; + public static final String VMWARE = "VMWARE"; + @Autowired Persons persons; + + @Autowired TransactionTemplate txTemplate; + + @Autowired TenantIdentifierResolver currentTenant; + + @Test + void saveAndLoadPerson() { + + createPerson(PIVOTAL, "Adam"); + createPerson(VMWARE, "Eve"); + + currentTenant.setCurrentTenant(VMWARE); + assertThat(persons.findAll()).extracting(Person::getName).containsExactly("Eve"); + + currentTenant.setCurrentTenant(PIVOTAL); + assertThat(persons.findAll()).extracting(Person::getName).containsExactly("Adam"); + } + + private Person createPerson(String schema, String name) { + + currentTenant.setCurrentTenant(schema); + + Person adam = txTemplate.execute(tx -> { + Person person = Persons.named(name); + return persons.save(person); + }); + + assertThat(adam.getId()).isNotNull(); + return adam; + } +} diff --git a/jpa/pom.xml b/jpa/pom.xml index e162733a..1fbd5b6d 100644 --- a/jpa/pom.xml +++ b/jpa/pom.xml @@ -27,6 +27,7 @@ security showcase vavr + multitenant