具有单个域对象和多个数据源的 SpringBoot

Cod*_*kie 5 java spring spring-boot

我已经阅读了很多关于拥有多个数据源的帖子,但我觉得我的情况可能有点独特,因为我不是在寻求设置多个数据源的帮助,而是帮助配置多个数据源以使用单个域(实体)对象。

用例场景

我们有两个相同的财务系统,但我的组织中的数据除外,其中每个系统代表公司的不同部门。每个部门都有一个完全独立的数据库,具有相同的模式。我必须构建一个应用程序来连接两个数据库。当用户登录时,他们将选择他们需要访问的公司的哪个部门并继续他们的数据请求。基于包含分区的查询参数,应用程序需要在域对象中选择正确的数据源并拉回适当的数据。

在 groovy/grails 中,我能够拥有一个包含多个数据源的域。

例子。

static mapping = {
    datasources (['datasourceA','datasourceB'])
}
Run Code Online (Sandbox Code Playgroud)

并且基于查询参数,我能够确定要使用的数据源。

例子

Person."${division.datasource}".findAllByRunId
Run Code Online (Sandbox Code Playgroud)

我想知道如何在 SpringBoot 2.2.0 中实现相同的行为?

数据库

Finance_System_A (datasourceA)
  - Person: 
      - Name: John
      - ID: 1

Finance_System_B (datasourceB)
  - Person: 
      - Name: Dave
      - ID: 1
Run Code Online (Sandbox Code Playgroud)

SpringBoot 应用程序

SpringBoot Person Domain
  - Person:
      - Name:
      - ID: 
Run Code Online (Sandbox Code Playgroud)

查询示例(grails 风格)

Person.{"datasourceA"}.findById(1) = John
Person.{"datasourceB"}.findById(1) = Dave
Run Code Online (Sandbox Code Playgroud)

Cod*_*kie 4

我设法想出了几个解决方案来完成这项任务。

选项 1 - 多租户

在我看来,多租户方法似乎是最干净的方法,同时仍然允许每个租户拥有自己的数据库。

目录结构

org.company.project
    - ApplicationMain
        |_config
            - DatasourceConfiguration
            - WebMvcConfig
        |_routing
            - TenantContext
            - TenantInterceptor
            - TenantSourceRouter
        |_domain
            - Person
        |_repository
            |_ PersonRepository
        |_web
            -APIController
Run Code Online (Sandbox Code Playgroud)

数据源配置

@Configuration
@EnableTransactionManagement
public class DatasourceConfiguration {

    @Resource
    private Environment env;

    @Bean
    public DataSource dataSource() {
        AbstractRoutingDataSource dataSource = new TenantSourceRouter();

        Map<Object, Object> targetDataSources = new HashMap<>();

        targetDataSources.put("ALBANY", albanyDatasource());
        targetDataSources.put("BUFFALO", buffaloDatasource());

        dataSource.setTargetDataSources(targetDataSources);
        dataSource.setDefaultTargetDataSource(albanyDatasource());

        return dataSource;
    }

    public DataSource albanyDatasource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(env.getProperty("company.datasource.driver-class-name"));
        dataSource.setUrl(env.getProperty("company.datasource.albany.jdbc-url"));
        dataSource.setUsername(env.getProperty("company.datasource.albany.username"));
        dataSource.setPassword(env.getProperty("company.datasource.albany.password"));

        return dataSource;
    }

    public DataSource buffaloDatasource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName(env.getProperty("company.datasource.driver-class-name"));
        dataSource.setUrl(env.getProperty("company.datasource.buffalo.jdbc-url"));
        dataSource.setUsername(env.getProperty("company.datasource.buffalo.username"));
        dataSource.setPassword(env.getProperty("company.datasource.buffalo.password"));

        return dataSource;
    }

}
Run Code Online (Sandbox Code Playgroud)

域实体 - 人

@Entity
public class Person {

    @Id
    private String id;

    private String name;
} 
Run Code Online (Sandbox Code Playgroud)

人员存储库

public interface PersonRepository extends JpaRepository<Person, String> {

}
Run Code Online (Sandbox Code Playgroud)

租户上下文

public class TenantContext {

    private static final ThreadLocal<String> currentTenant  = new ThreadLocal<>();

    public static void setCurrentTenant(String tenant) {
        Assert.notNull(tenant, "clientDatabase cannot be null");
        currentTenant.set(tenant);
    }

    public static String getClientDatabase() {
        return currentTenant .get();
    }

    public static void clear() {
        currentTenant .remove();
    }

}
Run Code Online (Sandbox Code Playgroud)

租户上下文

public class TenantSourceRouter extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return TenantContext.getClientDatabase();
    }
}
Run Code Online (Sandbox Code Playgroud)

TenantInterceptor - 我决定添加一个全局拦截器,您可以在其中使用所需租户“ALBANY”或“BUFFALO”设置请求标头“X-TenantID”,而不必在控制器操作的基础上处理此问题。

@Component
public class TenantInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        String tenantId = request.getHeader("X-TenantID");
        TenantContext.setCurrentTenant(tenantId);
        return true;
    }
    @Override
    public void postHandle(
            HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
        TenantContext.clear();
    }

}
Run Code Online (Sandbox Code Playgroud)

WebMvcConfig - 现在我们必须向 WebMvc 注册拦截器

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TenantInterceptor());
    }

}
Run Code Online (Sandbox Code Playgroud)

APIController - 最后我们创建控制器,我们将在其中访问我们的存储库。

@RestController
@RequestMapping("/api")
public class APIController {

    @Autowired
    private PersonRepository personRepository;

    @GetMapping("/{id}")
    public Optional<Person> get(@PathVariable String id) {
        return personRepository.findById(id);
    }

    @GetMapping("/")
    public List<Person> getAll() {
        return personRepository.findAll();
    }

}
Run Code Online (Sandbox Code Playgroud)

应用程序.yml

company:
  datasource:
    driver-class-name: com.microsoft.sqlserver.jdbc.SQLServerDriver
    albany:
      jdbc-url: ***
      username: ***
      password: ***
    buffalo:
      jdbc-url: ***
      username: ***
      password: ***
Run Code Online (Sandbox Code Playgroud)

选项 2 - 具有多个存储库的更传统的多租户

目录结构

org.company.project
    - ApplicationMain
        |_config
            - AlbanyDbConfiguration (datasource 1)
            - BuffaloDbConfiguration (datasource 2)
        |_domain
            - Person
        |_repository
            |_ albany
                - PersonRepositoryAlbany (repository for datasource 1)
            |_ buffalo
                - PersonRepositoryBuffalo (repository for datasource 2)
        |_web
            -APIController
Run Code Online (Sandbox Code Playgroud)

应用程序.yml

spring:
  datasource:
    jdbc-url: ***
    username: ***
    password: ***
buffalo:
  datasource:
    jdbc-url: ***
    username: ***
    password: ***
Run Code Online (Sandbox Code Playgroud)

域实体 - 人

@Entity
public class Person {

    @Id
    private String id;

    private String name;
}
Run Code Online (Sandbox Code Playgroud)

存储库 - PersonRepositoryAlbany*

public interface PersonRepositoryAlbany extends JpaRepository<Person, String>, JpaSpecificationExecutor<Person> {

}
Run Code Online (Sandbox Code Playgroud)

存储库 - PersonRepositoryBuffalo*

public interface PersonRepositoryBuffalo extends JpaRepository<Person, String>, JpaSpecificationExecutor<Person> {

}
Run Code Online (Sandbox Code Playgroud)

数据源配置 - AlbanyDbConfiguration

@Configuration
@EnableJpaRepositories(
        basePackages = { "org.company.project.repository.albany"},
        entityManagerFactoryRef = "albanyEntityManagerFactory",
        transactionManagerRef = "albanyTransactionManager")
public class AlbanyDbConfiguration {

    @Primary
    @Bean(name = "dataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "albanyEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean
        entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("dataSource") DataSource dataSource) {
        return builder
                .dataSource(dataSource)
                .packages("org.company.project.domain")
                .properties(jpaProperties())
                .build();
    }

    public Map<String, Object> jpaProperties() {
        Map<String, Object> props = new HashMap<>();
        props.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
        props.put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName());
        return props;
    }

    @Primary
    @Bean(name = "albanyTransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("albanyEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }

}
Run Code Online (Sandbox Code Playgroud)

数据源配置 - BuffaloDbConfiguration

@Configuration
@EnableJpaRepositories(
        basePackages = { "org.company.project.repository.buffalo"},
        entityManagerFactoryRef = "buffaloEntityManagerFactory",
        transactionManagerRef = "buffaloTransactionManager")
public class BuffaloDbConfiguration {

    @Bean(name = "buffaloDataSource")
    @ConfigurationProperties(prefix = "buffalo.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "buffaloEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean
    entityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("buffaloDataSource") DataSource dataSource) {
        return builder
                .dataSource(dataSource)
                .packages("org.company.project.domain")
                .properties(jpaProperties())
                .build();
    }

    public Map<String, Object> jpaProperties() {
        Map<String, Object> props = new HashMap<>();
        props.put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
        props.put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName());
        return props;
    }

    @Bean(name = "buffaloTransactionManager")
    public PlatformTransactionManager transactionManager(@Qualifier("buffaloEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}
Run Code Online (Sandbox Code Playgroud)

Web 控制器 - APIController

@EnableTransactionManagement
@RestController
@RequestMapping("/api")
public class APIController {

    @Autowired
    private PersonRepositoryAlbany personRepositoryAlbany;

    @Autowired
    private PersonRepositoryBuffalo personRepositoryBuffalo;

    @GetMapping("/albany")
    public List<Person> albany() {
        return getPersonsAlbany();
    }

    @GetMapping("/buffalo")
    public List<Person> buffalo() {
        return getPersonsBuffalo();
    }

    @Transactional("albanyTransactionManager")
    public List<Person> getPersonsAlbany() {
        return personRepositoryAlbany.findAll();
    }

    @Transactional("buffaloTransactionManager")
    public List<Person> getPersonsBuffalo() {
        return personRepositoryBuffalo.findAll();
    }

}
Run Code Online (Sandbox Code Playgroud)