Как настроить Hibernate Session для нескольких источников данных? - PullRequest
0 голосов
/ 17 апреля 2020

В мультитенантном проекте Spring-Boot (2.2.6) с Spring-Data-JPA и «Hibernate» (5.4.12) я настроил два источника данных:

  1. Общий
  2. Изолированный

Источник данных Общий имеет дело с Объекты, которые являются общими для всех Арендаторов, в то время как Изолированный Источник данных обрабатывает объекты, принадлежащие определенному Арендатору. Поэтому, следуя этому руководству , я организовал свои пакеты следующим образом, чтобы разделить два мира:

Packages

Это мой класс 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 ?

...