Spring boot JPA 下套用多個資料庫來源

Spring boot JPA 下套用多個資料庫來源

2022, Sep 12    

最近一個新的專案需要同時動用到兩個不同的資料庫,雖然是同一台機器,但是 JPA 的連線要包含資料庫資訊,也就是說需要可以支援一個以上的資料庫連線才行,這邊就簡單講解下怎麼設定

application.yml

以往過去都是透過 application.yml 直接撰寫套用連線設定如下

spring:
  datasource:
    host: localhost
    port: 5432
    database: mydb
    password: 123456
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://${spring.datasource.host}:${spring.datasource.port}/${spring.datasource.database}?useUnicode=true&characterEncoding=utf-8
    username: postgres
    hikari:
      maximum-pool-size: 1000
      minimum-idle: 10
      connection-timeout: 10000
      max-lifetime: 1800000
      idle-timeout: 600000
      auto-commit: true
  jpa:
    database: POSTGRESQL
    show-sql: true
    open-in-view: true
    properties:
      hibernate:
        default_schema: testt
        dialect: org.hibernate.dialect.PostgreSQLDialect
        temp:
          use_jdbc_metadata_defaults: false

但是我們現在要切分多個 datasource 那這個結構需要調整一下

spring:
  datasource:
    mydb1:
      host: localhost
      port: 5432
      schema: testt
      database: mydb1
      password: 123456
      jdbc-url: jdbc:postgresql://${spring.datasource.host}:${spring.datasource.port}/${spring.datasource.database}?useUnicode=true&characterEncoding=utf-8
    mydb2:
      host: localhost
      port: 5432
      schema: testt
      database: mydb2
      password: 123456
      jdbc-url: jdbc:postgresql://${spring.datasource.host}:${spring.datasource.port}/${spring.datasource.database}?useUnicode=true&characterEncoding=utf-8

要注意的是 url 要改成 jdbc-url 實際的可用屬性可以參考 spring.datasource.hikari. 下的屬性

根據 spring.datasource 下一層去切分不同的 datasource 裡面屬性則依照 spring.datasource.hikari 去給予,然後加一下 spring 的 configuration,有兩個要加

  • Mydb1Config.java
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef = "mydb1EntityManagerFactory",
    transactionManagerRef = "mydb1TransactionManager",
    basePackages = {"springboot.demo.model.mydb1.dao"})
@Configuration
public class Mydb1Config {

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

  @Bean("mydb1EntityManagerFactory")
  @Primary
  public LocalContainerEntityManagerFactoryBean historyEntityManagerFactory(
      EntityManagerFactoryBuilder builder,
      @Qualifier("mydb1DataSource") DataSource dataSource
  ) {
    HashMap<String, Object> properties = new HashMap<>();
    properties.put("hibernate.physical_naming_strategy",
        "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
    properties.put("hibernate.implicit_naming_strategy",
        "org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy");
    properties.put("hibernate.auto_quote_keyword", "true");

    var bean = builder.dataSource(dataSource)
        .properties(properties)
        .packages("springboot.demo.model.mydb1.entity")
        .persistenceUnit("mydb1").build();

    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setShowSql(false);
    bean.setJpaVendorAdapter(vendorAdapter);

    return bean;
  }

  @Bean("mydb1TransactionManager")
  @Primary
  public PlatformTransactionManager historyTransactionManager(
      @Qualifier("mydb1EntityManagerFactory") EntityManagerFactory entityManagerFactory
  ) {
    return new JpaTransactionManager(entityManagerFactory);
  }
}

  • Mydb2Config.java
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef = "mydb2EntityManagerFactory",
    transactionManagerRef = "mydb2TransactionManager",
    basePackages = {"springboot.demo.model.mydb2.dao"})
@Configuration
public class Mydb2Config {

  @Bean("mydb2DataSource")
  @ConfigurationProperties(prefix = "spring.datasource.mydb2")
  public DataSource historyDataSource() {
    return DataSourceBuilder.create().build();
  }

  @Bean("mydb2EntityManagerFactory")
  public LocalContainerEntityManagerFactoryBean historyEntityManagerFactory(
      EntityManagerFactoryBuilder builder,
      @Qualifier("mydb2DataSource") DataSource dataSource
  ) {
    HashMap<String, Object> properties = new HashMap<>();
    properties.put("hibernate.physical_naming_strategy",
        "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
    properties.put("hibernate.implicit_naming_strategy",
        "org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy");
    properties.put("hibernate.auto_quote_keyword", "true");

    var bean = builder.dataSource(dataSource)
        .properties(properties)
        .packages("springboot.demo.model.mydb2.entity")
        .persistenceUnit("mydb2").build();

    HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
    vendorAdapter.setShowSql(false);
    bean.setJpaVendorAdapter(vendorAdapter);

    return bean;
  }

  @Bean("mydb2TransactionManager")
  public PlatformTransactionManager historyTransactionManager(
      @Qualifier("mydb2EntityManagerFactory") EntityManagerFactory entityManagerFactory
  ) {
    return new JpaTransactionManager(entityManagerFactory);
  }
}

其實最重點要注意到的就是,建立各自的 DataSource 以及指定各自 entity 以及 dao 放的 package 會根據這裡的設定去進行掃描,然後各種其餘的設定都可以透過 Map 去 put 進去,參數就參考官方使用

特別要注意的是預設這裡的命名策略是完全按照 entity 以及 field 的名稱而不是 jpa 的預設規則,會去幫忙轉換命名風格成底線命名,因此這裡特別設定的兩個屬性,就是要還原成熟悉的 jpa 使用方式

並且設定好 auto_quote_keyword 讓保留字也可以用於欄位

HashMap<String, Object> properties = new HashMap<>();
    properties.put("hibernate.physical_naming_strategy",
        "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
    properties.put("hibernate.implicit_naming_strategy",
        "org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy");
    properties.put("hibernate.auto_quote_keyword", "true");

其他還要注意多個 datasource configuration 必須要有一個的 bean 全部都要加上 @Primary

這些都搞定之後就跟往常操作沒兩樣囉