Adds examples of Spring Data JPA with Hibernate Multitenancy.
This commit is contained in:
33
jpa/multitenant/.gitignore
vendored
Normal file
33
jpa/multitenant/.gitignore
vendored
Normal file
@@ -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/
|
||||
10
jpa/multitenant/README.adoc
Normal file
10
jpa/multitenant/README.adoc
Normal file
@@ -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.
|
||||
|
||||
45
jpa/multitenant/db/pom.xml
Normal file
45
jpa/multitenant/db/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>hibernate-multitenant-db</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data.examples</groupId>
|
||||
<artifactId>spring-data-jpa-hibernate-multitenant-examples</artifactId>
|
||||
<version>2.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Hibernate Multitenant DB</name>
|
||||
<description>
|
||||
Example project demonstrating the integration of Hibernates Multitenant feature with Spring Boot using separate databases.
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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> T unwrap(Class<T> aClass) {
|
||||
throw new UnsupportedOperationException("Can't unwrap this.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Map<String, Object> hibernateProperties) {
|
||||
hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, this);
|
||||
}
|
||||
}
|
||||
@@ -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 + '\'' + '}';
|
||||
}
|
||||
}
|
||||
@@ -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<Person, Long> {
|
||||
|
||||
static Person named(String name) {
|
||||
|
||||
Person person = new Person();
|
||||
person.setName(name);
|
||||
return person;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> hibernateProperties) {
|
||||
hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
|
||||
}
|
||||
}
|
||||
@@ -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<Object, Object> 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();
|
||||
}
|
||||
}
|
||||
2
jpa/multitenant/db/src/main/resources/manual-schema.sql
Normal file
2
jpa/multitenant/db/src/main/resources/manual-schema.sql
Normal file
@@ -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));
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
45
jpa/multitenant/partition/pom.xml
Normal file
45
jpa/multitenant/partition/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>hibernate-multitenant-partition</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data.examples</groupId>
|
||||
<artifactId>spring-data-jpa-hibernate-multitenant-examples</artifactId>
|
||||
<version>2.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Hibernate Multitenant Partition</name>
|
||||
<description>
|
||||
Example project demonstrating the integration of Hibernates Multitenant feature with Spring Boot using partitioned tables.
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 + '\'' + '}';
|
||||
}
|
||||
}
|
||||
@@ -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<Person, Long> {
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -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<String, Object> hibernateProperties) {
|
||||
hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
|
||||
}
|
||||
}
|
||||
2
jpa/multitenant/partition/src/main/resources/schema.sql
Normal file
2
jpa/multitenant/partition/src/main/resources/schema.sql
Normal file
@@ -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));
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
51
jpa/multitenant/pom.xml
Normal file
51
jpa/multitenant/pom.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.springframework.data.examples</groupId>
|
||||
<artifactId>spring-data-jpa-hibernate-multitenant-examples</artifactId>
|
||||
<version>2.0.0.BUILD-SNAPSHOT</version>
|
||||
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.0.0-M4</version>
|
||||
</parent>
|
||||
|
||||
<name>Hibernate Multitenant Examples</name>
|
||||
<description>
|
||||
A set of projects demonstrating how Hibernates Multitenant feature can be integrated with Spring Boot and Spring Data JPA.
|
||||
</description>
|
||||
|
||||
<modules>
|
||||
<module>partition</module>
|
||||
<module>schema</module>
|
||||
<module>db</module>
|
||||
</modules>
|
||||
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
</project>
|
||||
45
jpa/multitenant/schema/pom.xml
Normal file
45
jpa/multitenant/schema/pom.xml
Normal file
@@ -0,0 +1,45 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>hibernate-multitenant-schema</artifactId>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.data.examples</groupId>
|
||||
<artifactId>spring-data-jpa-hibernate-multitenant-examples</artifactId>
|
||||
<version>2.0.0.BUILD-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<name>Hibernate Multitenant Schema</name>
|
||||
<description>
|
||||
Example project demonstrating the integration of Hibernates Multitenant feature with Spring Boot using different schema.
|
||||
</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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> T unwrap(Class<T> aClass) {
|
||||
throw new UnsupportedOperationException("Can't unwrap this.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Map<String, Object> hibernateProperties) {
|
||||
hibernateProperties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, this);
|
||||
}
|
||||
}
|
||||
@@ -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 + '\'' + '}';
|
||||
}
|
||||
}
|
||||
@@ -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<Person, Long> {
|
||||
static Person named(String name) {
|
||||
Person person = new Person();
|
||||
person.setName(name);
|
||||
return person;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> hibernateProperties) {
|
||||
hibernateProperties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, this);
|
||||
}
|
||||
}
|
||||
8
jpa/multitenant/schema/src/main/resources/schema.sql
Normal file
8
jpa/multitenant/schema/src/main/resources/schema.sql
Normal file
@@ -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));
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@
|
||||
<module>security</module>
|
||||
<module>showcase</module>
|
||||
<module>vavr</module>
|
||||
<module>multitenant</module>
|
||||
</modules>
|
||||
|
||||
<profiles>
|
||||
|
||||
Reference in New Issue
Block a user