Spring Boot + JPAで複数DBに接続しようとするとnaming strategyが変わる


JPAでの複数のデータソースへの接続は、https://www.baeldung.com/spring-data-jpa-multiple-databases のようにConfiguration Classを設定することで実現できます。

しかし、Springのデフォルトのnaming strategyが適用されなくなるため、これを明示的に指定する必要があります。

なお、以下ではDBはMySQLを、JPAの実装にはHibernateを用いた場合を想定しています。
対応方法だけ知りたい方は[naming strategyを設定する](#naming-strategy%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B)を参照ください。


## デフォルトのnaming strategy

Spring Bootでのnaming strategyは、spring.jpa.hibernate.naming.physical-strategyとspring.jpa.hibernate.naming.implicit-strategyで設定されます。

それぞれデフォルトではorg.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategyとorg.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategyが設定されており、

- .(ドット)を_(アンダースコア)に置換
- CamelCaseをsnake_caseに変換
- table名をLower caseに変換

となります。

例えば、このようなスキーマのテーブルに対しては、

```mysql
mysql> desc user_master;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| id         | varchar(255) | NO   | PRI | NULL    |       |
| first_name | varchar(255) | YES  |     | NULL    |       |
| last_name  | varchar(255) | YES  |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+
```

以下のようなEntityを定義することで自動で対応されます。(逆も然りで、このようなEntityを定義してddl-auto=createを設定すれば上記のようなテーブルが作成されます。)

```java
@Entity
public class UserMaster {

    @Id
    private String id;

    private String firstName;
    
    private String lastName;
}
```

### 実際に確認する
上記のEntityを定義し、spring.datasourceで接続先の情報を設定、さらに適当なJpaRepositoryを実装したクラスとControllerを実装してみます。

application.yml

```yaml
spring:
  datasource:
    url: jdbc:mysql://your.mysql.host:3306/sample
    username: user
    password: xxxx
    driver-class-name: com.mysql.cj.jdbc.Driver
```
repository

```java
@Repository
public interface UserMasterRepository extends JpaRepository<UserMaster, String> {
}
```
controller

```java
@RequestMapping("test")
@RestController
public class TestController {

    private UserMasterRepository userMasterRepository;

    public TestController(UserMasterRepository userMasterRepository) {
        this.userMasterRepository = userMasterRepository;
    }

    @GetMapping("")
    public UserMaster test() {
        return userMasterRepository.findAll();
    }
}
```

→ 実際にエンドポイントを叩いてみるとテーブル名・カラム名がいい感じに変換され、レコードを引っ張ってこれることが確認できます。

```sh
$ curl -s localhost:8080/test | jq
[
  {
    "id": "johndoe",
    "firstName": "john",
    "lastName": "doe"
  }
]
```

### 自前で定義した場合は適用されない

spring.datasourceで設定していたものを以下のように自前で設定してみます。

application.yml

```yaml
spring:
  datasource:
    sample:
      url: jdbc:mysql://your.mysql.host:3306/sample
      username: user
      password: xxxx
      driver-class-name: com.mysql.cj.jdbc.Driver
```

```java
@Configuration
@EnableJpaRepositories(
        basePackages = "com.example.demo.repository",
        entityManagerFactoryRef = "myEntityManager",
        transactionManagerRef = "myTransactionManager"
)
public class DataSourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.sample")
    public DataSourceProperties myDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    @Primary
    @Autowired
    public DataSource myDataSource(@Qualifier("myDataSourceProperties") DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }

    @Bean
    @Primary
    @Autowired
    public LocalContainerEntityManagerFactoryBean myEntityManager(EntityManagerFactoryBuilder builder, @Qualifier("myDataSource") DataSource dataSource){
        return builder.dataSource(dataSource)
                .packages("com.example.demo.model")
                .build();
    }

    @Bean
    @Primary
    @Autowired
    public JpaTransactionManager myTransactionManager(@Qualifier("myEntityManager") LocalContainerEntityManagerFactoryBean primaryEntityManager) {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(primaryEntityManager.getObject());
        return transactionManager;
    }
}
```

先ほどのエンドポイントにリクエストを飛ばしてみると以下のエラーが発生しました。

```
java.sql.SQLSyntaxErrorException: Table 'sample.usermaster' doesn't exist
  at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120) ~[mysql-connector-java-8.0.18.jar:8.0.18]
  ...
```
ログの通り、"UserMaster"が"usermaster (not user_master)"に変換されており、SpringのデフォルトのNamingStrategyが適用されていないことがわかります。



## naming strategyを設定する
EntityManagerFactoryBuilderでLocalContainerEntityManagerFactoryBeanをbuildする際にJpaのプロパティを渡せるので、デフォルトと同じ設定を反映させてみましょう。

```java
public LocalContainerEntityManagerFactoryBean myEntityManager(EntityManagerFactoryBuilder builder, @Qualifier("myDataSource") DataSource dataSource){
  return builder.dataSource(dataSource)
    .packages("com.example.demo.model")
    .properties(new HashMap<>(){
      {
        put("hibernate.physical_naming_strategy", SpringPhysicalNamingStrategy.class.getName());
        put("hibernate.implicit_naming_strategy", SpringImplicitNamingStrategy.class.getName());
      }
    })
    .build();
}
```

これで再度叩いてみると、

```sh
$ curl -s localhost:8080/test | jq
[
  {
    "id": "johndoe",
    "firstName": "john",
    "lastName": "doe"
  }
]
```

無事接続できました。


指摘・補足等あれば是非コメントいただければ。

## 参考
https://www.baeldung.com/spring-data-jpa-multiple-databases
https://www.baeldung.com/hibernate-field-naming-spring-boot
https://stackoverflow.com/questions/40509395/cant-set-jpa-naming-strategy-after-configuring-multiple-data-sources-spring-1