Mastering Spring Data: Configuring Dual Entity Managers for Read-Replies

- Published on
Mastering Spring Data: Configuring Dual Entity Managers for Read-Replies
In the world of modern application development, efficiencies in data access can directly correlate to enhanced performance and user experience. One of the powerful features offered by Spring Data is the ability to configure multiple entity managers. This article explores how to set up dual entity managers in a Spring application to optimize data access, especially in read-replicas scenarios.
Understanding the Basics
What are Entity Managers?
An Entity Manager in the context of JPA (Java Persistence API) is the primary interface for interacting with the persistence context. It is responsible for managing the lifecycle of entity instances and the operations (CRUD) that can be performed on them. By having multiple entity managers, developers can direct read and write operations to different datasources, increasing scalability and improving performance.
Why Dual Entity Managers?
Managing reads and writes separately allows for:
- Increased Performance: By directing read traffic to replicas, you can significantly reduce load on the primary database.
- Fault Tolerance: If one data source fails, the application can still function using the other.
- Data Synchronization: This setup can allow for more advanced database configurations like sharding and partitioning.
Setting Up a Spring Boot Project
Let's start with the creation of a multi-module Spring Boot project. For this setup, we’ll consider a scenario where we have a primary database for writes and a read-replica for reads.
Dependencies
In your pom.xml
(for Maven users), include the necessary dependencies:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
Application Properties
You need to define your primary and replica datasource configurations in your application.properties
.
# Primary DataSource (Write)
spring.datasource.primary.url=jdbc:h2:mem:primary;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.primary.username=sa
spring.datasource.primary.password=
# Read Replica Data Source
spring.datasource.replica.url=jdbc:h2:mem:replica;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.replica.username=sa
spring.datasource.replica.password=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
Entity Configuration
We will set up two separate entity manager factories and transaction managers—one for writes and one for reads.
Primary Data Source Configuration
Create a class named PrimaryDataSourceConfig
.
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.primary",
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
public class PrimaryDataSourceConfig {
@Bean(name = "primaryDataSource")
public DataSource primaryDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl("jdbc:h2:mem:primary;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
dataSource.setDriverClassName("org.h2.Driver");
return dataSource;
}
@Bean(name = "primaryEntityManagerFactory")
public EntityManagerFactory primaryEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(primaryDataSource())
.packages("com.example.entity.primary")
.persistenceUnit("primary")
.build();
}
@Bean(name = "primaryTransactionManager")
public PlatformTransactionManager primaryTransactionManager(
@Qualifier("primaryEntityManagerFactory") EntityManagerFactory primaryEntityManagerFactory) {
return new JpaTransactionManager(primaryEntityManagerFactory);
}
}
Replica Data Source Configuration
Similarly, create a class named ReplicaDataSourceConfig
.
@Configuration
@EnableJpaRepositories(
basePackages = "com.example.repository.replica",
entityManagerFactoryRef = "replicaEntityManagerFactory",
transactionManagerRef = "replicaTransactionManager"
)
public class ReplicaDataSourceConfig {
@Bean(name = "replicaDataSource")
public DataSource replicaDataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setUrl("jdbc:h2:mem:replica;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
dataSource.setDriverClassName("org.h2.Driver");
return dataSource;
}
@Bean(name = "replicaEntityManagerFactory")
public EntityManagerFactory replicaEntityManagerFactory(
EntityManagerFactoryBuilder builder) {
return builder
.dataSource(replicaDataSource())
.packages("com.example.entity.replica")
.persistenceUnit("replica")
.build();
}
@Bean(name = "replicaTransactionManager")
public PlatformTransactionManager replicaTransactionManager(
@Qualifier("replicaEntityManagerFactory") EntityManagerFactory replicaEntityManagerFactory) {
return new JpaTransactionManager(replicaEntityManagerFactory);
}
}
Creating Entities and Repositories
Entity Classes
Define your entity classes in their respective packages.
Primary Entity
package com.example.entity.primary;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Getters and setters
}
Replica Entity
For simplicity, let’s assume our replica entity can mirror the primary one.
package com.example.entity.replica;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class UserReplica {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// Getters and setters
}
Repositories
In your repository packages, create the interfaces for repositories.
package com.example.repository.primary;
import com.example.entity.primary.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
package com.example.repository.replica;
import com.example.entity.replica.UserReplica;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserReplicaRepository extends JpaRepository<UserReplica, Long> {
}
Executing Transactions
To illustrate the dual entity manager functionality, you can create services for interacting with both databases.
Service Class Example
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserRepository userRepository;
private final UserReplicaRepository userReplicaRepository;
@Autowired
public UserService(UserRepository userRepository, UserReplicaRepository userReplicaRepository) {
this.userRepository = userRepository;
this.userReplicaRepository = userReplicaRepository;
}
@Transactional("primaryTransactionManager")
public User createUser(String name) {
User user = new User();
user.setName(name);
return userRepository.save(user);
}
public List<UserReplica> getAllUsers() {
return userReplicaRepository.findAll();
}
}
In the createUser
method, we explicitly specify the transaction manager to ensure that operations related to user creation go to the primary database. However, in the getAllUsers
method, we perform reads from the replica.
Testing the Setup
To verify your configuration, you can create a simple REST controller.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
@Autowired
public UserController(UserService userService) {
this.userService = userService;
}
@PostMapping
public User createUser(@RequestBody String name) {
return userService.createUser(name);
}
@GetMapping
public List<UserReplica> getUsers() {
return userService.getAllUsers();
}
}
Closing the Chapter
By implementing dual entity managers in your Spring Boot application, you can efficiently manage read and write operations to separate data sources. This configuration not only boosts performance but also provides resilience to your data access layer.
To learn more about Spring Data and JPA, check Spring Data documentation.
With this foundational knowledge, you are well on your way to mastering the intricacies of Spring Data. Experiment with more complex queries, relationships, and even external data sources to enhance your application's data handling capabilities. The key lies in understanding how your data interacts within the context of your application, optimizing for both performance and scalability. Happy coding!
Checkout our other articles