Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
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.
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.
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.
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.xmlEstamos 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.
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.
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.javaLeitura:
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.javaApó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.propertiesAgora com nossas propriedades devidamente configuradas, podemos começar a configurar nossos Datasources.
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.javaA 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.
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.javaRepository 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.javaRepository 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.javaController:
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.javaAgora 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"
}'
TerminalAgora podemos ler os registros para ver se tudo aconteceu como esperado.
curl --request GET \
--url http://localhost:8080/games
TerminalEssa 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.