diff --git a/pom.xml b/pom.xml index 0edcf6a..dc858d2 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,11 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.cloud + spring-cloud-starter-openfeign + true + org.springframework.boot spring-boot-starter-amqp diff --git a/src/main/java/com/knecon/fforesight/databasetenantcommons/DatabaseTenantCommonsAutoConfiguration.java b/src/main/java/com/knecon/fforesight/databasetenantcommons/DatabaseTenantCommonsAutoConfiguration.java index 82415b4..c1d1898 100644 --- a/src/main/java/com/knecon/fforesight/databasetenantcommons/DatabaseTenantCommonsAutoConfiguration.java +++ b/src/main/java/com/knecon/fforesight/databasetenantcommons/DatabaseTenantCommonsAutoConfiguration.java @@ -2,6 +2,7 @@ package com.knecon.fforesight.databasetenantcommons; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.ComponentScan; @@ -11,6 +12,7 @@ import com.knecon.fforesight.tenantcommons.MultiTenancyAutoConfiguration; @ComponentScan @AutoConfiguration @AutoConfigureAfter(MultiTenancyAutoConfiguration.class) +@ImportAutoConfiguration(MultiTenancyAutoConfiguration.class) @EnableConfigurationProperties(TenantHikariSettings.class) public class DatabaseTenantCommonsAutoConfiguration { diff --git a/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/DynamicDataSourceBasedMultiTenantConnectionProvider.java b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/DynamicDataSourceBasedMultiTenantConnectionProvider.java index 05794e4..e33389f 100644 --- a/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/DynamicDataSourceBasedMultiTenantConnectionProvider.java +++ b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/DynamicDataSourceBasedMultiTenantConnectionProvider.java @@ -9,6 +9,7 @@ import javax.sql.DataSource; import org.hibernate.engine.jdbc.connections.spi.AbstractDataSourceBasedMultiTenantConnectionProviderImpl; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; import com.google.common.cache.CacheBuilder; diff --git a/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/MultiTenantDataSourceHealthIndicator.java b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/MultiTenantDataSourceHealthIndicator.java index 167090e..1ca443f 100644 --- a/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/MultiTenantDataSourceHealthIndicator.java +++ b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/MultiTenantDataSourceHealthIndicator.java @@ -3,10 +3,12 @@ package com.knecon.fforesight.databasetenantcommons.providers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.jdbc.DataSourceHealthIndicator; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; +@Primary @Component -public class MultiTenantDataSourceHealthIndicator extends DataSourceHealthIndicator { +public class MultiTenantDataSourceHealthIndicator extends DataSourceHealthIndicator { @Autowired private MultiTenantDataSource multiTenantDataSource; @@ -21,7 +23,7 @@ public class MultiTenantDataSourceHealthIndicator extends DataSourceHealthIndic @Override - protected void doHealthCheck(Health.Builder builder) throws Exception { + protected void doHealthCheck(Health.Builder builder) { builder.up().withDetail("database", "multi-tenant-setup"); } diff --git a/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantPersistenceConfig.java b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantPersistenceConfig.java new file mode 100644 index 0000000..ea63aa7 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantPersistenceConfig.java @@ -0,0 +1,95 @@ +package com.knecon.fforesight.databasetenantcommons.providers; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.cfg.AvailableSettings; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; +import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.orm.hibernate5.SpringBeanContainer; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; +import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; + +import com.knecon.fforesight.databasetenantcommons.providers.properties.TenantHikariSettings; +import com.knecon.fforesight.tenantcommons.EncryptionDecryptionService; +import com.knecon.fforesight.tenantcommons.TenantProvider; + +import jakarta.persistence.EntityManagerFactory; +import lombok.RequiredArgsConstructor; + +@Configuration +@EnableJpaRepositories(basePackages ="${multitenancy.packages.repositories:}", entityManagerFactoryRef = "tenantEntityManagerFactory", transactionManagerRef = "tenantTransactionManager") +@EnableConfigurationProperties({JpaProperties.class, TenantHikariSettings.class}) +@RequiredArgsConstructor +public class TenantPersistenceConfig { + + + + + @Value("${multitenancy.packages.entities:}") + private String[] entityPackages; + private final JpaProperties jpaProperties; + + + @Primary + @Bean + public LocalContainerEntityManagerFactoryBean tenantEntityManagerFactory(DynamicDataSourceBasedMultiTenantConnectionProvider connectionProvider, + CurrentTenantIdentifierResolverImpl tenantResolver) { + + LocalContainerEntityManagerFactoryBean emfBean = new LocalContainerEntityManagerFactoryBean(); + emfBean.setPersistenceUnitName("tenant-persistence-unit"); + emfBean.setPackagesToScan(entityPackages!=null ? entityPackages : new String[0]); + emfBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); + Map properties = new HashMap<>(this.jpaProperties.getProperties()); + properties.put(AvailableSettings.PHYSICAL_NAMING_STRATEGY, "org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy"); + properties.put(AvailableSettings.IMPLICIT_NAMING_STRATEGY, "org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy"); +// properties.put(AvailableSettings.MULTI_TENANT, MultiTenancyStrategy.DATABASE); + properties.put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, connectionProvider); + properties.put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantResolver); + properties.put(AvailableSettings.DIALECT, "org.hibernate.dialect.PostgreSQLDialect"); + properties.put("hibernate.temp.use_jdbc_metadata_defaults",false); + emfBean.setJpaPropertyMap(properties); + + return emfBean; + } + + + @Primary + @Bean + public JpaTransactionManager tenantTransactionManager(@Qualifier("tenantEntityManagerFactory") EntityManagerFactory emf) { + + JpaTransactionManager tenantTransactionManager = new JpaTransactionManager(); + tenantTransactionManager.setEntityManagerFactory(emf); + return tenantTransactionManager; + } + + + @Bean + @ConfigurationProperties("multitenancy.tenant.liquibase") + public LiquibaseProperties tenantLiquibaseProperties() { + + return new LiquibaseProperties(); + } + + + @Bean + public TenantSpringLiquibaseExecutor tenantLiquibase(EncryptionDecryptionService encryptionService, + TenantProvider tenantProvider, + LiquibaseProperties tenantLiquibaseProperties) { + + return new TenantSpringLiquibaseExecutor(encryptionService, tenantProvider, tenantLiquibaseProperties); + } + +} diff --git a/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantSpringLiquibaseExecutor.java b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantSpringLiquibaseExecutor.java new file mode 100644 index 0000000..30338d8 --- /dev/null +++ b/src/main/java/com/knecon/fforesight/databasetenantcommons/providers/TenantSpringLiquibaseExecutor.java @@ -0,0 +1,87 @@ +package com.knecon.fforesight.databasetenantcommons.providers; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.util.List; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ResourceLoader; +import org.springframework.jdbc.datasource.SingleConnectionDataSource; + +import com.knecon.fforesight.databasetenantcommons.providers.utils.JDBCUtils; +import com.knecon.fforesight.tenantcommons.EncryptionDecryptionService; +import com.knecon.fforesight.tenantcommons.TenantProvider; +import com.knecon.fforesight.tenantcommons.model.TenantResponse; + +import feign.RetryableException; +import liquibase.integration.spring.SpringLiquibase; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@RequiredArgsConstructor +public class TenantSpringLiquibaseExecutor implements InitializingBean, ResourceLoaderAware { + + private final EncryptionDecryptionService encryptionService; + private final TenantProvider tenantProvider; + + @Qualifier("tenantLiquibaseProperties") + private final LiquibaseProperties tenantLiquibaseProperties; + + @Setter + private ResourceLoader resourceLoader; + + + @Override + public void afterPropertiesSet() { + + log.info("DynamicDataSources based multi-tenancy enabled"); + try { + this.runOnAllTenants(tenantProvider.getTenants()); + }catch (RetryableException e){ + log.warn("Tenant Service not online, skipping liquibase migration: {}", e.getMessage(), e); + } + } + + + @SneakyThrows + protected void runOnAllTenants(List tenants) { + + for (var tenant : tenants) { + + var jdbcURL = JDBCUtils.buildJdbcUrlWithSchema(tenant.getDatabaseConnection()); + log.info("Initializing Liquibase for tenant {} / {}", tenant.getTenantId(), jdbcURL); + try (Connection connection = DriverManager.getConnection(jdbcURL, + tenant.getDatabaseConnection().getUsername(), + encryptionService.decrypt(tenant.getDatabaseConnection().getPassword()))) { + DataSource tenantDataSource = new SingleConnectionDataSource(connection, false); + SpringLiquibase liquibase = this.getSpringLiquibase(tenantDataSource); + liquibase.setDefaultSchema(tenant.getDatabaseConnection().getSchema()); + liquibase.setLiquibaseSchema(tenant.getDatabaseConnection().getSchema()); + liquibase.afterPropertiesSet(); + } catch (Exception e) { + log.error("Failed to run liquibase migration on tenant: {}", tenant.getTenantId()); + } + log.info("Liquibase ran for tenant " + tenant.getTenantId()); + } + } + + + protected SpringLiquibase getSpringLiquibase(DataSource dataSource) { + + SpringLiquibase liquibase = new SpringLiquibase(); + liquibase.setResourceLoader(resourceLoader); + liquibase.setDataSource(dataSource); + liquibase.setChangeLog(tenantLiquibaseProperties.getChangeLog()); + liquibase.setContexts(tenantLiquibaseProperties.getContexts()); + return liquibase; + } + +}