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

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

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

· 5 мин.

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

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

По вымышленному ТЗ нам необходимо написать небольшое 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

Заключение

Создавая дополнительные конфигурации вы можете настраивать соединения не только с двумя базами данных, но и с большим количеством.

Рекомендуемые Статьи

Откат изменений Liquibase
· 9 мин.
Что такое Java Collection Framework?
· 14 мин.
Создаем блог на Ghost
· 11 мин.
Реализация JWT в Spring Boot
· 10 мин.
Что такое JWT токен?
· 9 мин.