Spring Data JPA

 In enterprise applications, it’s common to have separate database tables for staging and main database schema, especially for maker/checker approval workflows. In such cases, both tables often share a very similar structure but may require separate entities for specific use cases. This article demonstrates how to implement this in a Spring Boot application using a generic approach with base entities and services.

Why to Use Base Entities?

By creating base entities for each domain (e.g., User and Product), you can:

  • Share common fields and logic between Main and Staging tables.
  • Allow entity-specific customizations without duplicating code.
  • Maintain a clean and extensible architecture.

Step-by-Step Implementation

We’ll implement the following components:

  1. Base entities for shared fields.
  2. Separate entities for Main and Staging tables.
  3. Generic repositories and services to handle common CRUD operations.
  4. Specific repositories, services, and controllers for each entity type.

1. Define the Base Entities

Base entities contain shared fields common to both Main and Staging tables.

Base Entity for User :

@MappedSuperclass
public abstract class BaseUserEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}

Base Entity for Product

@MappedSuperclass
public abstract class BaseProductEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Double price;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}

2. Extend Base Entities for Main and Staging

The Main and Staging entities inherit common fields from their respective base entities and can include specific fields as needed.

MainUserEntity


@Entity
@Table(name = "main_user")
public class MainUserEntity extends BaseUserEntity {
// Additional fields specific to Main User (if any)
}

StagingUserEntity

@Entity
@Table(name = "staging_user")
public class StagingUserEntity extends BaseUserEntity {
// Additional fields specific to Staging User (if any)
}

MainProductEntity

@Entity
@Table(name = "main_product")
public class MainProductEntity extends BaseProductEntity {
// Additional fields specific to Main Product (if any)
}

StagingProductEntity

@Entity
@Table(name = "staging_product")
public class StagingProductEntity extends BaseProductEntity {
// Additional fields specific to Staging Product (if any)
}

3. Create Generic Base Repository

Define a generic repository that handles common CRUD operations for any entity.

@NoRepositoryBean
public interface BaseRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
// Add custom methods if required
}

4. Define Repositories for Each Entity

User Repositories

@Repository
public interface MainUserRepository extends BaseRepository<MainUserEntity, Long> {}
@Repository
public interface StagingUserRepository extends BaseRepository<StagingUserEntity, Long> {}

Product Repositories

@Repository
public interface MainProductRepository extends BaseRepository<MainProductEntity, Long> {}
@Repository
public interface StagingProductRepository extends BaseRepository<StagingProductEntity, Long> {}

5. Create Generic Base Service

The BaseService encapsulates common business logic for all entities.

public abstract class BaseService<T> {
private final BaseRepository<T, Long> repository;
protected BaseService(BaseRepository<T, Long> repository) {
this.repository = repository;
}
public List<T> getAll() {
return repository.findAll();
}
public T save(T entity) {
return repository.save(entity);
}
public Optional<T> getById(Long id) {
return repository.findById(id);
}
public void delete(Long id) {
repository.deleteById(id);
}
}

6. Extend Base Service for Each Type

User Services

@Service
public class MainUserService extends BaseService<MainUserEntity> {
public MainUserService(MainUserRepository repository) {
super(repository);
}
}
@Service
public class StagingUserService extends BaseService<StagingUserEntity> {
public StagingUserService(StagingUserRepository repository) {
super(repository);
}
}

Product Services

@Service
public class MainProductService extends BaseService<MainProductEntity> {
public MainProductService(MainProductRepository repository) {
super(repository);
}
}
@Service
public class StagingProductService extends BaseService<StagingProductEntity> {
public StagingProductService(StagingProductRepository repository) {
super(repository);
}
}

7. Define Controllers

The controllers route HTTP requests to their respective service layers.

User Controller

@RestController
@RequestMapping("/api/users")
public class UserController {

private final MainUserService mainUserService;
private final StagingUserService stagingUserService;
public UserController(MainUserService mainUserService, StagingUserService stagingUserService) {
this.mainUserService = mainUserService;
this.stagingUserService = stagingUserService;
}
@GetMapping("/main")
public List<MainUserEntity> getAllMainUsers() {
return mainUserService.getAll();
}
@GetMapping("/staging")
public List<StagingUserEntity> getAllStagingUsers() {
return stagingUserService.getAll();
}
}

Product Controller

@RestController
@RequestMapping("/api/products")
public class ProductController {
private final MainProductService mainProductService;
private final StagingProductService stagingProductService;
public ProductController(MainProductService mainProductService, StagingProductService stagingProductService) {
this.mainProductService = mainProductService;
this.stagingProductService = stagingProductService;
}
@GetMapping("/main")
public List<MainProductEntity> getAllMainProducts() {
return mainProductService.getAll();
}
@GetMapping("/staging")
public List<StagingProductEntity> getAllStagingProducts() {
return stagingProductService.getAll();
}
}

8. Database Setup

Create the Main and Staging tables for both User and Product:

CREATE TABLE main_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255)
);

CREATE TABLE staging_user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255)
);

CREATE TABLE main_product (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
price DOUBLE
);
CREATE TABLE staging_product (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
price DOUBLE
);

Benefits of This Approach

  1. Separation of Concerns: Clear distinction between Main and Staging entities.
  2. Reusability: Shared logic in base entities and services reduces duplication.
  3. Extensibility: Easily add new fields or entity types.
  4. Maintainability: Centralized logic ensures consistency and simplifies updates.

This architecture is ideal for applications requiring robust workflows for staging and main environments. By following this approach, you’ll achieve a clean, extensible, and maintainable codebase.

A Guide to Common Annotations in this example

Spring Boot provides a variety of annotations that help simplify the development of Java-based applications, particularly for web and database-driven applications. Here’s a summary of what each annotation does:

  • @MappedSuperclass: Used for defining a base class with common fields for other entities.
  • @Entity and @Table: Used to map a Java class to a database table.
  • @NoRepositoryBean: Prevents Spring Data from creating a bean for a base repository.
  • @Repository: Marks a class as a repository, enabling it for database operations.
  • @Service: Marks a class as a service, typically containing business logic.
  • @RestController: Defines a controller that handles HTTP requests and returns data in RESTful APIs.

These annotations help streamline the development of Spring Boot applications by reducing boilerplate code and providing a clear structure for various layers of the application. Understanding and utilizing these annotations effectively will enhance your ability to build robust, maintainable, and scalable applications.

These annotations play critical roles in various layers of Spring Boot applications, from database interaction to building RESTful APIs. Let’s dive into each annotation to understand their significance and usage.

1. @MappedSuperclass

The @MappedSuperclass annotation is part of Java Persistence API (JPA) and is used when you want to define a base class that provides common properties and behavior for other entity classes, but the class itself should not be mapped to a database table.

Purpose:

  • It allows for inheritance of common attributes and methods without mapping the superclass itself to a database table.
  • The fields in a @MappedSuperclass will be inherited by subclasses, which will then be mapped to a database table.

2. @Entity and @Table

The @Entity annotation marks a class as an entity that should be mapped to a database table in JPA. It represents a table in the relational database and is used in conjunction with @Table, which provides additional configuration like the table name.

Purpose:

  • @Entity is used to define a JPA entity, and each instance of the class will correspond to a row in the table.
  • @Table is used to specify the name of the database table or additional configuration, like indexes or unique constraints.

3. @NoRepositoryBean

The @NoRepositoryBean annotation is used in Spring Data JPA to mark a repository interface or class as a base repository that should not be instantiated as a bean. It is typically used for shared repository logic.

Purpose:

  • It prevents Spring Data from automatically creating a repository bean from the interface or class.
  • It allows for the creation of a common base repository interface with shared methods that can be inherited by other repository interfaces

4. @Repository

The @Repository annotation is used to define a DAO (Data Access Object) that interacts with the database. It marks a class as a Spring Data repository, making it a candidate for component scanning and dependency injection.

Purpose:

  • It defines a repository bean that provides CRUD operations for an entity.
  • It enables automatic exception translation, converting database-related exceptions into Spring’s DataAccessException.

5. @Service

The @Service annotation is used to mark a class as a service that contains business logic. It is a specialization of the @Component annotation and is often used for service classes in the service layer of an application.

Purpose:

  • It defines a service bean that contains the business logic.
  • It can be injected into other components such as controllers to manage application logic.

6. @RestController

The @RestController annotation is used to create RESTful web services in Spring Boot. It is a specialization of the @Controller annotation, and it automatically serializes the return value of methods into JSON (or other formats).

Purpose:

  • It defines a controller that handles HTTP requests and returns data directly in the HTTP response body.
  • It is commonly used to build REST APIs.

Post a Comment

Previous Post Next Post