Comments (1)
This is a really simple implementation, you probably need to adjust it.
I first defined some JSON, that holds the Tenant Configuration:
[
{
"tenant" : "TenantOne",
"dataSourceClassName": "org.postgresql.ds.PGSimpleDataSource",
"url": "jdbc:postgresql://127.0.0.1:5432/sampledb",
"user": "philipp",
"password": "test_pwd"
},
{
"tenant": "TenantTwo",
"dataSourceClassName": "org.postgresql.ds.PGSimpleDataSource",
"url": "jdbc:postgresql://127.0.0.1:5432/sampledb2",
"user": "philipp",
"password": "test_pwd"
}
]
Then I wrote a Matching class, called DatabaseConfiguration
:
package de.bytefish.multitenancy.routing.config;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Objects;
public class DatabaseConfiguration {
private final String tenant;
private final String url;
private final String user;
private final String dataSourceClassName;
private final String password;
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
public DatabaseConfiguration(@JsonProperty("tenant") String tenant,
@JsonProperty("url") String url,
@JsonProperty("user") String user,
@JsonProperty("dataSourceClassName") String dataSourceClassName,
@JsonProperty("password") String password) {
this.tenant = tenant;
this.url = url;
this.user = user;
this.dataSourceClassName = dataSourceClassName;
this.password = password;
}
@JsonProperty("tenant")
public String getTenant() {
return tenant;
}
@JsonProperty("url")
public String getUrl() {
return url;
}
@JsonProperty("user")
public String getUser() {
return user;
}
@JsonProperty("dataSourceClassName")
public String getDataSourceClassName() {
return dataSourceClassName;
}
@JsonProperty("password")
public String getPassword() {
return password;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DatabaseConfiguration that = (DatabaseConfiguration) o;
return Objects.equals(tenant, that.tenant) &&
Objects.equals(url, that.url) &&
Objects.equals(user, that.user) &&
Objects.equals(dataSourceClassName, that.dataSourceClassName) &&
Objects.equals(password, that.password);
}
@Override
public int hashCode() {
return Objects.hash(tenant, url, user, dataSourceClassName, password);
}
}
And finally I wrote a can write a DynamicTenantAwareRoutingSource
, that reads from a given file and checks this file every five seconds. If the configuration for a Tenant has changed, then the DataSource is closed and the new one is created:
package de.bytefish.multitenancy.routing;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zaxxer.hikari.HikariDataSource;
import de.bytefish.multitenancy.core.ThreadLocalStorage;
import de.bytefish.multitenancy.routing.config.DatabaseConfiguration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.scheduling.annotation.Scheduled;
import javax.sql.DataSource;
import java.io.File;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class DynamicTenantAwareRoutingSource extends AbstractRoutingDataSource {
private final String filename;
private final ObjectMapper objectMapper;
private final Map<String, HikariDataSource> tenants;
public DynamicTenantAwareRoutingSource(String filename) {
this(filename, new ObjectMapper());
}
public DynamicTenantAwareRoutingSource(String filename, ObjectMapper objectMapper) {
this.filename = filename;
this.objectMapper = objectMapper;
this.tenants = getDataSources();
}
@Override
public void afterPropertiesSet() {
// Nothing to do ..
}
@Override
protected DataSource determineTargetDataSource() {
String lookupKey = (String) determineCurrentLookupKey();
// And finally return it:
return tenants.get(lookupKey);
}
@Override
protected Object determineCurrentLookupKey() {
return ThreadLocalStorage.getTenantName();
}
private Map<String, HikariDataSource> getDataSources() {
// Deserialize the JSON:
DatabaseConfiguration[] configurations = getDatabaseConfigurations();
// Now create a Lookup Table:
return Arrays
.stream(configurations)
.collect(Collectors.toMap(x -> x.getTenant(), x -> buildDataSource(x)));
}
private DatabaseConfiguration[] getDatabaseConfigurations() {
try {
return objectMapper.readValue(new File(filename), DatabaseConfiguration[].class);
} catch(Exception e) {
throw new RuntimeException(e);
}
}
private HikariDataSource buildDataSource(DatabaseConfiguration configuration) {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setInitializationFailTimeout(0);
dataSource.setMaximumPoolSize(5);
dataSource.setDataSourceClassName(configuration.getDataSourceClassName());
dataSource.addDataSourceProperty("url", configuration.getUrl());
dataSource.addDataSourceProperty("user", configuration.getUser());
dataSource.addDataSourceProperty("password", configuration.getPassword());
return dataSource;
}
@Scheduled(fixedDelay = 5000L)
public void insertOrUpdateDataSources() {
DatabaseConfiguration[] configurations = getDatabaseConfigurations();
for (DatabaseConfiguration configuration : configurations) {
if (tenants.containsKey(configuration.getTenant())) {
HikariDataSource dataSource = tenants.get(configuration.getTenant());
// We only shutdown and reload, if the configuration has actually changed...
if (!isCurrentConfiguration(dataSource, configuration)) {
// Make sure we close this DataSource first...
dataSource.close();
// ... and then insert a new DataSource:
tenants.put(configuration.getTenant(), buildDataSource(configuration));
}
} else {
tenants.put(configuration.getTenant(), buildDataSource(configuration));
}
}
}
private boolean isCurrentConfiguration(HikariDataSource dataSource, DatabaseConfiguration configuration) {
return Objects.equals(dataSource.getDataSourceProperties().getProperty("user"), configuration.getUser())
&& Objects.equals(dataSource.getDataSourceProperties().getProperty("url"), configuration.getUrl())
&& Objects.equals(dataSource.getDataSourceProperties().getProperty("password"), configuration.getPassword())
&& Objects.equals(dataSource.getDataSourceClassName(), configuration.getDataSourceClassName());
}
}
If I put the JSON in D:\tenants.json
, then you can create it like this. Any changes to the configuration will trigger either a new connection, if it hasn't been present before or shutdown / recreate a Connection Pool:
package de.bytefish.multitenancy;
import com.zaxxer.hikari.HikariDataSource;
import de.bytefish.multitenancy.routing.DynamicTenantAwareRoutingSource;
import de.bytefish.multitenancy.routing.TenantAwareRoutingSource;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
@SpringBootApplication
@EnableScheduling
@EnableTransactionManagement
public class SampleJerseyApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
new SampleJerseyApplication()
.configure(new SpringApplicationBuilder(SampleJerseyApplication.class))
.properties(getDefaultProperties())
.run(args);
}
@Bean
public DataSource dataSource() {
return new DynamicTenantAwareRoutingSource("D:\\tenants.json");
}
private static Properties getDefaultProperties() {
Properties defaultProperties = new Properties();
// Set sane Spring Hibernate properties:
defaultProperties.put("spring.jpa.show-sql", "true");
defaultProperties.put("spring.jpa.hibernate.naming.physical-strategy", "org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl");
defaultProperties.put("spring.datasource.initialize", "false");
// Prevent JPA from trying to Auto Detect the Database:
defaultProperties.put("spring.jpa.database", "postgresql");
// Prevent Hibernate from Automatic Changes to the DDL Schema:
defaultProperties.put("spring.jpa.hibernate.ddl-auto", "none");
return defaultProperties;
}
}
from bytefish.de.
Related Issues (20)
- TransactionalTestBase package? HOT 1
- Thank you for the excellent article. Unfortunately, it did not work for me. HOT 2
- Question: Usage of bytea HOT 1
- Sql minor bug HOT 1
- Spring Boot Multitenancy guide - Error with AbstractRoutingDataSource HOT 3
- Query on spring abstractroutingdatasource advantage HOT 2
- Question on dynamic load of the data source HOT 10
- Add a flow diagram HOT 7
- NEST not working HOT 1
- Adding null in AuditQueryResultUtil HOT 3
- How the tenant can be changed on runtime HOT 1
- Can't write CLR type System.DateTime to database type timestamp with time zone HOT 1
- AbstractMap used by PersonMap in SqlMapper blog entry doesn't match java.util.AbstractMap HOT 7
- Update async multi-tenant example HOT 3
- Select tenant HOT 1
- Subject: Parsing Command Line Arguments in .NET || Topic: Add basic arg validation HOT 2
- How to addnew datasource without @Scheduled HOT 1
- How can we do the reverting to a particular revision using Versioning and Auditing with spring data Envers? please provider me an example for reverting HOT 2
- Tables are not getting created while dynamically tenant details in tenant.json HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from bytefish.de.