Spring Data: Como Acessar Múltiplas Bases de Dados na mesma aplicação

Nesse artigo vamos ensinar como conectar o Spring Data a múltiplas bases de dados, permitindo que a sua aplicação conecte-se com bancos diferentes, ou o mesmo conectando a um cluster de banco de dados que possua conexões diferentes para leitura e escrita.

Motivação

Algumas vezes podemos nos deparar com o seguinte problema, conectar nossa aplicação a mais de um banco de dados, com o Spring Data as configurações padrão prevê que você conecte a um único banco. Mas com algumas classes de configuração podemos não só conectar a bases de dados diferentes, como conectar a mesma base com endpoints diferentes, um exemplo dessa última é quando você se conecta a um cluster de banco de dados que se utiliza de read replicas, esse tipo de solução nos provê dois endpoints de conexão um somente para leitura e outro para escrita. A solução proposta aqui atenderá tando conexões com bancos diferentes como conexões com o mesmo banco por endpoints diferentes.

Explicando nosso exemplo

No exemplo que estamos trabalhando temos uma aplicação que se conectará a um cluster de banco de dados que nos provê dois endpoints um de escrita e outro de leitura, vamos separar as camadas necessárias para que possamos criar dois repository, cada qual com a sua função específica, leitura ou escrita.

Instalando as dependências do Spring Data

Para nossa abordagem vamos utilizar como exemplo bancos Postgres, e estamos utilizando uma aplicação Spring Boot com gerenciador de pacotes Maven. Segue as dependências necessárias:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
	<groupId>org.postgresql</groupId>
	<artifactId>postgresql</artifactId>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-configuration-processor</artifactId>
	<optional>true</optional>
</dependency>
pom.xml

Estamos instalando o Spring Data, juntamente com o driver do Postgres e também o Configuration Processor.
O Configuration Processor ele é importante pois vamos criar classes de configurações para poder configurar nossa conexão através do application.properties.

Implementando

Com as dependências devidamente instaladas, vamos começar a configurar nossas conexões, para isso vamos precisar criar dois Datasources diferentes, assim como dois EntityManagerFactory e também a configuração do nosso Transaction Manager.

Configurando a classes de propriedades

Vamos criar uma classe para armazenar as propriedades da nossa conexão de escrita e outra para a conexão de leitura:

Escrita:

package com.artefatox.multipledatabase.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import lombok.Data;

@Data
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.write")
public class WriteDatasourceProperties {
 
    private String url;
    private String username;
    private String password;
}
WriteDataSourceProperties.java

Leitura:

package com.artefatox.multipledatabase.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

import lombok.Data;

@Data
@Configuration
@ConfigurationProperties(prefix = "spring.datasource.read")
public class ReadDatasourceProperties {
 
    private String url;
    private String username;
    private String password;
}
ReadDataSourceProperties.java

Após criar as duas classes de propriedades vamos adicionar as propriedades no nosso arquivo application.properties, lembrando que o ConfigurationProperties se orienta pelo prefix, então no nosso caso nosso arquivo ficaria assim:

spring.datasource.write.url=jdbc:postgresql://<host-write>:<port>/<database>
spring.datasource.write.username=<username>
spring.datasource.write.password=<password>

spring.datasource.read.url=jdbc:postgresql://<host-read>:<port>/<database>
spring.datasource.read.username=<username>
spring.datasource.read.password=<password>
application.properties

Agora com nossas propriedades devidamente configuradas, podemos começar a configurar nossos Datasources.

Configurando Datasource de Escrita

Na nossa classe de configuração vamos criar alguns Beans que o Spring Data utiliza, e como o nosso Datasource de escrita será o mais importante, vamos anotar os beans com @Primary, isso fará com que eles tenham prioridade quando for ocorrer a injeção de dependência.

package com.artefatox.multipledatabase.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import jakarta.persistence.EntityManagerFactory;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackages = "com.artefatox.multipledatabase.repository.write",
    entityManagerFactoryRef = "writeEntityManagerFactory",
    transactionManagerRef = "transcationManager")
public class WriteDatasourceConfiguration {

    @Autowired
    private WriteDatasourceProperties properties;

    @Primary
    @Bean(name = "writeDataSource")
    public DataSource writeDataSource() {
        return DataSourceBuilder.create()
        .url(properties.getUrl())
        .username(properties.getUsername())
        .password(properties.getPassword())
        .build();
    }
    
    @Primary
    @Bean(name = "writeEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean writeEntityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("writeDataSource") DataSource dataSource) {
        return builder.dataSource(dataSource)
            .packages("com.artefatox.multipledatabase.entity")
            .persistenceUnit("writePU")
            .build();
    }

    @Bean
    @Primary
    public PlatformTransactionManager transcationManager(@Qualifier("writeEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}
ReadDataSourceConfiguration.java

Configurando o Datasource de Leitura

A configuração desse é bem semelhante ao de escrita, vamos apenas garantir que o nome dos beans sejam diferentes e nesse caso ele não receberá a anotação @Primary.

package com.artefatox.multipledatabase.config;


import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import jakarta.persistence.EntityManagerFactory;

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
    basePackages = "com.artefatox.multipledatabase.repository.read",
    entityManagerFactoryRef = "readEntityManagerFactory",
    transactionManagerRef = "readTranscationManager")
public class ReadDatasourceConfiguration {

    @Autowired
    private ReadDatasourceProperties properties;

    @Bean(name = "readDataSource")
    public DataSource readDataSource() {
        return DataSourceBuilder.create()
        .url(properties.getUrl())
        .username(properties.getUsername())
        .password(properties.getPassword())
        .build();
    }
    
    @Bean(name = "readEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean readEntityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("readDataSource") DataSource dataSource) {
        return builder.dataSource(dataSource)
            .packages("com.artefatox.multipledatabase.entity")
            .persistenceUnit("readPU")
            .build();
    }

    @Bean(name = "readTranscationManager")
    public PlatformTransactionManager readTranscationManager(@Qualifier("readEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}
ReadDatasourceConfiguration.java

É importante notar que cada Datasource aponta para um pacote específico dentro da nossa aplicação, dessa forma, todos os repositories que estiverem dentro do pacote write funcionaram com a conexão de escrita, e os read com a conexão de leitura.

Outro ponto é que essa separação não impede nossos repositories de fazer qualquer tipo de operação, se você pegar o de leitura verá que ele contém os métodos de escrita também, dessa forma eu poderia utilizar ao invés de uma base de dados com dois endpoints, dois bancos separados e as vezes com entidades diferentes, basta utilizar o mesmo padrão de configuração.

Testando nossa implementação

Para testar nossa implementação vamos criar uma entidade de exemplo com os devidos repositories e também um controller para que possamos interagir com a aplicação de uma maneira simples.

Entidade:

package com.artefatox.multipledatabase.entity;

import java.util.Date;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;

@Data
@Entity(name = "games")
public class Game {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Date lauch;
}
Game.java

Repository de Escrita:

package com.artefatox.multipledatabase.repository.write;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.artefatox.multipledatabase.entity.Game;

@Repository
public interface GameWriteRepository extends JpaRepository<Game, Long> {
    
}
GameWriteRepository.java

Repository de Leitura:

package com.artefatox.multipledatabase.repository.read;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.artefatox.multipledatabase.entity.Game;

@Repository
public interface GameReadRepository extends JpaRepository<Game, Long> {
    
}
GameReadRepository.java

Controller:

package com.artefatox.multipledatabase.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
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 com.artefatox.multipledatabase.entity.Game;
import com.artefatox.multipledatabase.repository.read.GameReadRepository;
import com.artefatox.multipledatabase.repository.write.GameWriteRepository;

import lombok.AllArgsConstructor;

@RestController
@RequestMapping("/games")
@AllArgsConstructor
public class GameController {

    private final GameWriteRepository writeRepository;
    private final GameReadRepository readRepository;

    @PostMapping
    @Transactional
    public ResponseEntity<?> create(@RequestBody Game game) {
        writeRepository.save(game);
        return ResponseEntity.status(HttpStatus.CREATED).body(game);    
    }

    @GetMapping
    public ResponseEntity<?> list() {
        List<Game> response = readRepository.findAll();
        return ResponseEntity.status(HttpStatus.OK).body(response);
    }
}
GameController.java

Agora podemos testar nossa aplicação, vamos fazer uma chama e ver se tudo se comporta da forma que esperamos, primeiro passo será criar um registro.

curl --request POST \
  --url http://localhost:8080/games \
  --header 'Content-Type: application/json' \
  --data '{
	"name": "Mortal Kombat",
	"lauch": "1992-08-01"
}'
Terminal

Agora podemos ler os registros para ver se tudo aconteceu como esperado.

curl --request GET \
  --url http://localhost:8080/games
Terminal

Conclusão

Essa técnica é bem interessante para muitas abordagens, tando para acessar diferentes base de dados, como para acessar uma mesma base com endpoints de escrita e leitura diferentes, da mesma forma que conectamos dois Datasources poderíamos conectar mais, mas vale lembrar que conectar uma mesma aplicação a muitos bancos pode não ser um boa prática, é importante ver a real necessidade disso, e considerar uma arquitetura de micros-serviços.

Links úteis

Mauricio Lima
Mauricio Lima

Bacharel em Ciência da Computação, profissional dedicado ao desenvolvimento de software e entusiasta da tecnologia.

Artigos: 65