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

Snippet of programming code in IDE
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!