Несколько баз данных для Spring Boot приложения

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

· 5 минуты на чтение
Несколько баз данных для Spring Boot приложения

Существование Spring Boot настолько упростило создание приложений, что разработчикам для подключении к базе данных уже даже не обязательно знать о существовании EntityManager. Поэтому работа сразу с двумя БД может оказаться не простой задачей.

Так как для подключения сразу двух баз данных в одно SpringBoot приложение одной конфигурацией не обойтись, необходимо руками прописать все создаваемые бины. SpringBoot здесь не помощник, вспоминаем старый добрый Spring. Давайте разбираться.

Спонсор поста

Постановка задачи

По вымышленному ТЗ нам необходимо написать небольшое CRUD приложение для правительства РФ, которое анализирует судебные дела. Проблема заключается в том, что данные об адвокатах у нас лежат в одной базе, а данные о судьях в другой.

Наше приложение будет максимально простым:

  • Две сущности. Два репозитория. Два REST контроллера;
  • CRUD операции;
  • Две базы данных: Postgres и H2;
😺
Проект на GitHub: multiple-databases-spring-boot

Создание классов

Для начала создадим наши сущности это Judge и Lawyer, которые наследуют Person.

Базовая сущность Person выглядит следующим образом:

package dev.struchkov.example.multipledatabases.domain;
import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.GenericGenerator; import javax.persistence.Column; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.MappedSuperclass; import java.util.UUID;
@Setter @Getter @MappedSuperclass public class Person { @Id @GeneratedValue(generator = "UUID") @GenericGenerator( name = "UUID", strategy = "org.hibernate.id.UUIDGenerator" ) @Column(name = "id", updatable = false, nullable = false) private UUID id; @Column private String firstName; @Column private String lastName; }

Сущность Lawyer выглядит аналогично сущности Judge, за исключением того, что они лежат в разных пакетах. Сущности для одной БД должны лежать в одном пакете, а сущности для второй БД в другом пакете.

Теперь создадим JPA репозитории, которые тоже положим в разные пакеты.

package dev.struchkov.example.multipledatabases.repository.dbone;
import dev.struchkov.example.multipledatabases.domain.entity.dbone.Judge; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.UUID;
@Repository public interface JudgeRepository extends JpaRepository<Judge, UUID> { }

И контроллеры для каждой сущности. Они могут лежать в одном пакете.

package dev.struchkov.example.multipledatabases.controller;
import dev.struchkov.example.multipledatabases.domain.entity.dbtwo.Lawyer; import dev.struchkov.example.multipledatabases.repository.dbtwo.LawyerRepository; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID;
@RestController @RequiredArgsConstructor @RequestMapping("api/lawyer") public class LawyerController { private final LawyerRepository lawyerRepository; @PostMapping public ResponseEntity<Lawyer> create(@RequestBody Lawyer lawyer) { return ResponseEntity.ok(lawyerRepository.save(lawyer)); } @GetMapping("{id}") public ResponseEntity<Lawyer> getById(@PathVariable UUID id) { return ResponseEntity.ok(lawyerRepository.findById(id).orElseThrow()); } @DeleteMapping("{id}") public HttpStatus delete(@PathVariable UUID id) { lawyerRepository.deleteById(id); return HttpStatus.OK; } }

Конфигурации БД

Теперь дело за конфигурацией для баз данных. Сначала создаем конфигурацию для Judge.

package dev.struchkov.example.multipledatabases.config;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap;
@EnableJpaRepositories( entityManagerFactoryRef = DatabaseOneConfig.ENTITY_MANAGER_FACTORY, transactionManagerRef = DatabaseOneConfig.TRANSACTION_MANAGER, basePackages = DatabaseOneConfig.JPA_REPOSITORY_PACKAGE ) @Configuration public class DatabaseOneConfig { public static final String PROPERTY_PREFIX = "app.dbone.datasource"; public static final String JPA_REPOSITORY_PACKAGE = "dev.struchkov.example.multipledatabases.repository.dbone"; public static final String ENTITY_PACKAGE = "dev.struchkov.example.multipledatabases.domain.entity.dbone"; public static final String ENTITY_MANAGER_FACTORY = "oneEntityManagerFactory"; public static final String DATA_SOURCE = "oneDataSource"; public static final String DATABASE_PROPERTY = "oneDatabaseProperty"; public static final String TRANSACTION_MANAGER = "oneTransactionManager"; @Bean(DATABASE_PROPERTY) @ConfigurationProperties(prefix = PROPERTY_PREFIX) public DatabaseProperty appDatabaseProperty() { return new DatabaseProperty(); } @Bean(DATA_SOURCE) public DataSource appDataSource( @Qualifier(DATABASE_PROPERTY) DatabaseProperty databaseProperty ) { return DataSourceBuilder .create() .username(databaseProperty.getUsername()) .password(databaseProperty.getPassword()) .url(databaseProperty.getUrl()) .driverClassName(databaseProperty.getClassDriver()) .build(); } @Bean(ENTITY_MANAGER_FACTORY) public LocalContainerEntityManagerFactoryBean appEntityManager( @Qualifier(DATA_SOURCE) DataSource dataSource ) { final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); em.setPersistenceUnitName(ENTITY_MANAGER_FACTORY); em.setPackagesToScan(ENTITY_PACKAGE); em.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); final HashMap<String, Object> properties = new HashMap<>(); properties.put("javax.persistence.validation.mode", "none"); properties.put("hibernate.hbm2ddl.auto", "update"); properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect"); em.setJpaPropertyMap(properties); return em; } @Bean(TRANSACTION_MANAGER) public PlatformTransactionManager sqlSessionTemplate( @Qualifier(ENTITY_MANAGER_FACTORY) LocalContainerEntityManagerFactoryBean entityManager, @Qualifier(DATA_SOURCE) DataSource dataSource ) { final JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManager.getObject()); transactionManager.setDataSource(dataSource); return transactionManager; } }

Чтобы работать с двумя базами нужно создать для каждой БД свой EntityManagerFactory и TransactionManager. А также указать, какие JPA репозитории и Entity относятся к этой БД.

В наших конфигурациях будут отличаться только строки 12-18 это переменные настройки:

  • PROPERTY_PREFIX – Префикс для конфигурации в файле application.property
  • JPA_REPOSITORY_PACKAGE – Путь до пакета, где лежат JPA репозитории для данной БД
  • ENTITY_PACKAGE – Путь до пакета, где лежат Entity
  • ENTITY_MANAGER_FACTORY – Название бина EntityManager
  • DATA_SOURCE – Название бина DataSource
  • DATABASE_PROPERTY – Название бина DatabaseProperty
  • TRANSACTION_MANAGER – Название бина TransactionManager

Названия для бинов нужны, чтобы спринг из двух вариантов выбирал правильный для конкретной БД.

DatabaseProperty

Первый бин DatabaseProperty отвечает за передачу данных из файла aplication.property для подключения к бд: пароль, логин, url и драйвер.

package dev.struchkov.example.multipledatabases.config;
import lombok.Getter; import lombok.Setter;
@Getter @Setter public class DatabaseProperty { private String url; private String username; private String password; private String classDriver; }

Создадим необходимые записи в aplication.property. Не забываем про PROPERTY_PREFIX.

app:
  dbone:
    datasource:
      class-driver: org.h2.Driver
      username: sa
      password: password
      url: jdbc:h2:mem:dbh2

DataSource

Теперь, когда у нас есть данные для подключения к БД создаем DataSource. С помощью @Qualifier указываем какой именно бин DatabaseProperty нам нужен.

EntityManager

Далее нам нужен EntityManagerFactoryBean.

  • В 44 строке мы указываем наш DataSource.
  • В 45 строке устанавливаем название для EntityManager.
  • В 46 строке мы передаем путь до пакета, в котором у нас лежат Entities.
  • А строки 49-53 позволяют задать проперти для Hibernate.

TransactionManager

Ну и последним шагом создаем бин PlatformTransactionManager. Для работы ему нужны бины EntityManager и Datasource.

Вторая база данных

Для второй базы данных будут отличаться только параметры настройки.

package dev.struchkov.example.multipledatabases.config;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; import org.springframework.transaction.PlatformTransactionManager; import javax.sql.DataSource; import java.util.HashMap;
@EnableJpaRepositories( entityManagerFactoryRef = DatabaseTwoConfig.ENTITY_MANAGER_FACTORY, transactionManagerRef = DatabaseTwoConfig.TRANSACTION_MANAGER, basePackages = DatabaseTwoConfig.JPA_REPOSITORY_PACKAGE ) @Configuration public class DatabaseTwoConfig { public static final String PROPERTY_PREFIX = "app.dbtwo.datasource"; public static final String JPA_REPOSITORY_PACKAGE = "dev.struchkov.example.multipledatabases.repository.dbtwo"; public static final String ENTITY_PACKAGE = "dev.struchkov.example.multipledatabases.domain.entity.dbtwo"; public static final String ENTITY_MANAGER_FACTORY = "twoEntityManagerFactory"; public static final String DATA_SOURCE = "twoDataSource"; public static final String DATABASE_PROPERTY = "twoDatabaseProperty"; public static final String TRANSACTION_MANAGER = "twoTransactionManager";
@Bean(DATABASE_PROPERTY) @ConfigurationProperties(prefix = PROPERTY_PREFIX) public DatabaseProperty appDatabaseProperty() { return new DatabaseProperty(); } @Bean(DATA_SOURCE) public DataSource appDataSource( @Qualifier(DATABASE_PROPERTY) DatabaseProperty databaseProperty ) { return DataSourceBuilder .create() .username(databaseProperty.getUsername()) .password(databaseProperty.getPassword()) .url(databaseProperty.getUrl()) .driverClassName(databaseProperty.getClassDriver()) .build(); } @Bean(ENTITY_MANAGER_FACTORY) public LocalContainerEntityManagerFactoryBean appEntityManager( @Qualifier(DATA_SOURCE) DataSource dataSource ) { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource); em.setPersistenceUnitName(ENTITY_MANAGER_FACTORY); em.setPackagesToScan(ENTITY_PACKAGE); em.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); HashMap<String, Object> properties = new HashMap<>(); properties.put("javax.persistence.validation.mode", "none"); properties.put("hibernate.hbm2ddl.auto", "update"); em.setJpaPropertyMap(properties); return em; } @Bean(TRANSACTION_MANAGER) public PlatformTransactionManager sqlSessionTemplate( @Qualifier(ENTITY_MANAGER_FACTORY) LocalContainerEntityManagerFactoryBean entityManager, @Qualifier(DATA_SOURCE) DataSource dataSource ) { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManager.getObject()); transactionManager.setDataSource(dataSource); return transactionManager; }
}

Указываем для поиска другие пакеты и другие названия бинов, а также указываем другой префикс для передачи параметров подключения к БД.

Записываем эти параметры в application.yml:

app:
  dbone:
    datasource:
      class-driver: org.h2.Driver
      username: sa
      password: password
      url: jdbc:h2:mem:dbh2
  dbtwo:
    datasource:
      class-driver: org.postgresql.Driver
      username: postgres
      password: 12345
      url: jdbc:postgresql://localhost:5432/dbpostgres

Резюмирую

Чтобы создать соединение сразу с несколькими базами данных, необходимо самостоятельно прописать необходимые бины для работы.

Struchkov Mark
Struchkov Mark
Задавайте вопросы, если что-то осталось не понятным👇