Мы нашли рабочее решение, которое представляет собой сочетание статических настроек источника данных для нашей central
базы данных и динамических настроек источника данных для базы данных наших клиентов.
По сути, мы точно знаем, из какой таблицы происходиткакая база данных.Следовательно, мы смогли разделить наши @Entity
классы на 2 разных пакета следующим образом.
com.ft.model
-- central
-- UserAccount.java
-- UserAccountRepo.java
-- customer
-- UserProfile.java
-- UserProfileRepo.java
Впоследствии мы создали два @Configuration
класса для настройки источника данных для каждого пакета.Для нашей базы данных central
мы используем статические настройки следующим образом.
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager",
basePackages = { "com.ft.model.central" }
)
public class CentralDatabaseConfiguration {
@Primary
@Bean(name = "dataSource")
public DataSource dataSource() {
return DataSourceBuilder.create(this.getClass().getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url("jdbc:sqlserver://localhost;databaseName=central")
.username("sa")
.password("mhsatuck")
.build();
}
@Primary
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("dataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.ft.model.central")
.persistenceUnit("central")
.build();
}
@Primary
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager (@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
Для @Entity
в пакете customer
мы устанавливаем динамический преобразователь источника данных, используя следующий @Configuration
.
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "customerEntityManagerFactory",
transactionManagerRef = "customerTransactionManager",
basePackages = { "com.ft.model.customer" }
)
public class CustomerDatabaseConfiguration {
@Bean(name = "customerDataSource")
public DataSource dataSource() {
return new MultitenantDataSourceResolver();
}
@Bean(name = "customerEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("customerDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.ft.model.customer")
.persistenceUnit("customer")
.build();
}
@Bean(name = "customerTransactionManager")
public PlatformTransactionManager transactionManager(@Qualifier("customerEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
В классе MultitenantDataSourceResolver
мы планируем сохранить Map
от созданного DataSource
, используя customerCode
в качестве ключа.Из каждого входящего запроса мы получим customerCode
и введем его в наш MultitenantDataSourceResolver
, чтобы получить правильный DataSource
в методе determineTargetDataSource()
.
public class MultitenantDataSourceResolver extends AbstractRoutingDataSource {
@Autowired
private Provider<CustomerWrapper> customerWrapper;
private static final Map<String, DataSource> dsCache = new HashMap<String, DataSource>();
@Override
protected Object determineCurrentLookupKey() {
try {
return customerWrapper.get().getCustomerCode();
} catch (Exception ex) {
return null;
}
}
@Override
protected DataSource determineTargetDataSource() {
String customerCode = (String) this.determineCurrentLookupKey();
if (customerCode == null)
return MultitenantDataSourceResolver.getDefaultDataSource();
else {
DataSource dataSource = dsCache.get(customerCode);
if (dataSource == null)
dataSource = this.buildDataSourceForCustomer();
return dataSource;
}
}
private synchronized DataSource buildDataSourceForCustomer() {
CustomerWrapper wrapper = customerWrapper.get();
if (dsCache.containsKey(wrapper.getCustomerCode()))
return dsCache.get(wrapper.getCustomerCode() );
else {
DataSource dataSource = DataSourceBuilder.create(MultitenantDataSourceResolver.class.getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url(wrapper.getJdbcUrl())
.username(wrapper.getDbUsername())
.password(wrapper.getDbPassword())
.build();
dsCache.put(wrapper.getCustomerCode(), dataSource);
return dataSource;
}
}
private static DataSource getDefaultDataSource() {
return DataSourceBuilder.create(CustomerDatabaseConfiguration.class.getClassLoader())
.driverClassName("com.microsoft.sqlserver.jdbc.SQLServerDriver")
.url("jdbc:sqlserver://localhost;databaseName=central")
.username("sa")
.password("mhsatuck")
.build();
}
}
CustomerWrapper
- это @RequestScope
объект, значения которого будут заполняться при каждом запросе @Controller
.Мы используем java.inject.Provider
, чтобы ввести его в наш MultitenantDataSourceResolver
.
Наконец, даже если логически, мы никогда не будем сохранять что-либо, используя значение по умолчанию DataSource
, потому что все запросы всегда будут содержать customerCode
, во время запуска нет customerCode
доступных.Следовательно, нам все еще нужно предоставить действительное значение по умолчанию DataSource
.В противном случае приложение не сможет запуститься.
Если у вас есть какие-либо комментарии или лучшее решение, пожалуйста, сообщите мне.