В мультитенантном проекте Spring-Boot (2.2.6) с Spring-Data-JPA и «Hibernate» (5.4.12) я настроил два источника данных:
- Общий
- Изолированный
Источник данных Общий имеет дело с Объекты, которые являются общими для всех Арендаторов, в то время как Изолированный Источник данных обрабатывает объекты, принадлежащие определенному Арендатору. Поэтому, следуя этому руководству , я организовал свои пакеты следующим образом, чтобы разделить два мира:
Это мой класс DatabaseConfiguration
:
package organization.database;
@Configuration
@EnableAutoConfiguration(
exclude = {
DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class,
HibernateJpaAutoConfiguration.class,
LiquibaseAutoConfiguration.class
}
)
public class DatabaseConfiguration {
}
Тогда моя конфигурация SharedDatabaseConfiguration:
package organization.database.shared;
@Configuration
@Getter
@Setter
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "organization.repository.shared",
entityManagerFactoryRef = "sharedEntityManagerFactory",
transactionManagerRef = "sharedTransactionManager"
)
@ConfigurationProperties(prefix = "organization.data")
public class SharedDatabaseConfiguration {
private Database database;
@Bean
@Primary
private PGSimpleDataSource sharedDataSource() {
PGSimpleDataSource ds = new PGSimpleDataSource();
ds.setServerNames(new String[]{this.database.getHost()});
ds.setPortNumbers(new int[]{this.database.getPort()});
ds.setDatabaseName(this.database.getDatabase());
ds.setCurrentSchema(this.database.getSchema());
ds.setUser(this.database.getUsername());
ds.setPassword(this.database.getPassword());
return ds;
}
@Bean
@Primary
public LocalContainerEntityManagerFactoryBean sharedEntityManagerFactory() {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(this.sharedDataSource());
em.setPersistenceUnitName("shared");
String sharedEntitiesPackage = Tenant.class.getPackageName();
em.setPackagesToScan(sharedEntitiesPackage);
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(adapter);
HashMap<String, Object> properties = new HashMap<>();
properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");
em.setJpaPropertyMap(properties);
return em;
}
@Bean
@Primary
public TransactionManager sharedTransactionManager() {
JpaTransactionManager transactionManager = new JpaTransactionManager();
EntityManagerFactory entityManagerFactory = this.sharedEntityManagerFactory().getObject();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}
и IsolatedDatabaseConfiguration
:
@Configuration
@EnableTransactionManagement
@RequiredArgsConstructor
@EnableJpaRepositories(
basePackages = "organization.repository.isolated",
entityManagerFactoryRef = "isolatedEntityManagerFactory",
transactionManagerRef = "isolatedTransactionManager"
)
public class IsolatedDatabaseConfiguration {
private final TenantService tenantService;
@Bean
@Qualifier("isolated")
public TenantDataSource<PGSimpleDataSource> isolatedDataSource() {
List<Tenant> tenants = this.tenantService.findAll();
System.out.println("DS Tenants: " + tenants.size());
Map<String, PGSimpleDataSource> targetDataSources = new ConcurrentHashMap<>();
tenants.stream().forEach(tenant -> {
PGSimpleDataSource ds = new PGSimpleDataSource();
ds.setServerNames(new String[]{tenant.getDatabase().getHost()});
ds.setPortNumbers(new int[]{tenant.getDatabase().getPort()});
ds.setDatabaseName(tenant.getDatabase().getDatabase());
ds.setCurrentSchema(tenant.getDatabase().getSchema());
ds.setUser(tenant.getDatabase().getUsername());
ds.setPassword(tenant.getDatabase().getPassword());
targetDataSources.put(tenant.getName(), ds);
});
TenantDataSource<PGSimpleDataSource> tenantDataSource = new TenantDataSource<>();
tenantDataSource.setDefaultDataSource(targetDataSources.values().iterator().next());
tenantDataSource.setDataSources(targetDataSources);
return tenantDataSource;
}
@Bean
@Qualifier("isolated")
public LocalContainerEntityManagerFactoryBean isolatedEntityManagerFactory(CurrentTenantIdentifierResolver resolver, MultiTenantConnectionProvider connectionProvider) {
Map<String, Object> jpaPropertiesMap = new HashMap<>();
jpaPropertiesMap.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
jpaPropertiesMap.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, resolver);
jpaPropertiesMap.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, connectionProvider);
jpaPropertiesMap.put(Environment.DIALECT, PostgreSQL9Dialect.class.getName());
jpaPropertiesMap.put(Environment.FORMAT_SQL, true);
jpaPropertiesMap.put(Environment.SHOW_SQL, false);
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setJpaPropertyMap(jpaPropertiesMap);
em.setDataSource(this.isolatedDataSource());
em.setPersistenceUnitName("isolated");
String isolatedEntitiesPackage = Employee.class.getPackageName();
em.setPackagesToScan(isolatedEntitiesPackage);
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(adapter);
return em;
}
@Bean
public TransactionManager isolatedTransactionManager(@Qualifier("isolated") LocalContainerEntityManagerFactoryBean factory) {
JpaTransactionManager transactionManager = new JpaTransactionManager();
EntityManagerFactory entityManagerFactory = factory.getObject();
transactionManager.setEntityManagerFactory(entityManagerFactory);
return transactionManager;
}
}
для обработки все различные источники данных об арендаторах (я собираюсь использовать изоляцию для нескольких арендаторов схемы с PostgreSQL). Я использую следующий класс TenantDataaSource
:
package organization.database.isolated;
public class TenantDataSource<D extends DataSource> extends AbstractDataSource {
@Getter
@Setter
private Map<String, D> dataSources;
@Getter
@Setter
private D defaultDataSource;
private TenantSchemaResolver tenantSchemaResolver = new TenantSchemaResolver();
public String getCurrentTenant() {
return this.tenantSchemaResolver.resolveCurrentTenantIdentifier();
}
public D getCurrentDataSource() {
return this.dataSources.getOrDefault(this.getCurrentTenant(), this.defaultDataSource);
}
public D getDataSource(String tenantID) {
return this.dataSources.getOrDefault(tenantID, this.defaultDataSource);
}
@Override
public Connection getConnection() throws SQLException {
return this.getCurrentDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return this.getCurrentDataSource().getConnection(username, password);
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
if (iface.isInstance(this.getCurrentDataSource())) {
return (T) this.getCurrentDataSource();
}
throw new SQLException("DataSource of type [" + this.getCurrentDataSource().getClass().getName() +
"] cannot be unwrapped as [" + iface.getName() + "]");
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return iface.isInstance(this.getCurrentDataSource());
}
}
Для настройки Hibernate для схемы с несколькими арендаторами, которую я реализовал следующие классы:
public class TenantSchemaResolver implements CurrentTenantIdentifierResolver {
private final TenantResolver tenantResolver = new TenantResolver();
@Override
public String resolveCurrentTenantIdentifier() {
return this.tenantResolver.resolve();
}
@Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
.
public class TenantConnectionProvider extends AbstractMultiTenantConnectionProvider {
private TenantDataSource<PGSimpleDataSource> dataSource;
private Map<String, DatasourceConnectionProviderImpl> connectionProviders;
public TenantConnectionProvider(TenantDataSource<PGSimpleDataSource> dataSource) {
this.dataSource = dataSource;
this.connectionProviders = new HashMap<>();
this.dataSource.getDataSources().forEach((key, value) -> {
final DatasourceConnectionProviderImpl connectionProvider = new DatasourceConnectionProviderImpl();
connectionProvider.setDataSource(value);
connectionProvider.configure(new HashMap());
this.connectionProviders.put(key, connectionProvider);
});
}
@Override
public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException {
if (this.connectionProviders.containsKey(tenantIdentifier)) {
DatasourceConnectionProviderImpl connectionProvider = this.connectionProviders.get(tenantIdentifier);
connectionProvider.closeConnection(connection);
}
}
@Override
public void releaseAnyConnection(Connection connection) throws SQLException {
connection.close();
}
@Override
protected ConnectionProvider getAnyConnectionProvider() {
return this.connectionProviders.values().iterator().next();
}
@Override
protected ConnectionProvider selectConnectionProvider(String tenantIdentifier) {
return this.connectionProviders.get(tenantIdentifier);
}
@Override
public Connection getConnection(String tenantIdentifier) throws SQLException {
return this.selectConnectionProvider(tenantIdentifier).getConnection();
}
}
При такой настройке моя проблема возникает, когда я выбираю объекты, которые имеют Lazy collections
в качестве поля, например:
package organization.domain.shared;
@Entity
@Table
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Tenant {
@Id
@GeneratedValue
@ToString.Exclude
private Long id;
@NotNull
@Column(unique = true, nullable = false)
private String name;
@ElementCollection
private Collection<URL> issuers;
@OneToOne(cascade = CascadeType.ALL)
private Database database;
}
ОБНОВЛЕНИЕ
Когда я пытаюсь получить доступ к полю LazyLoaded изнутри IsolatedDatabaseConfiguration
@Configuration, я получаю следующее ошибка:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: organization.domain.shared.Tenant.issuers, could not initialize proxy - no Session
Однако при попытке доступа к нему из метода контроллера (до возврата) поле LazyLoaded разрешается без проблем.
Моя идея состоит в том, что я должен подождать, пока SharedDatabaseConfiguration полностью загружен, прежде чем пытаться лениво загрузить что-либо из его сущностей. Если я прав, как я могу настроить это?
Как я могу настроить управление сеансами , сохраняя при этом EntityManagerFacories для обоих источников данных, из которых Isolated - это настройка AbstractRoutingDataSource ?