Adding JPA Entities

Let’s start by creating our JPA entities.

  1. Create a New Package:
    • Name the package entities (you could also use domain, but we’ll go with entities for now).
  2. Add Java Classes:
    • Create two classes: Beer and Customer.
    • These classes will mirror the properties of their corresponding DTOs exactly. Copy the properties from the DTOs into these classes.
  3. Refactoring and Annotations:
    • Place the id property at the top of the class for better organization (optional, but it makes the code cleaner).
    • Add Lombok annotations like @Data and @Builder to generate boilerplate code.
  4. Annotate as JPA Entities:
    • Mark these classes as @Entity.
    • Add the @Id annotation for the id property.
    • Add the @Version annotation for the version property.
  5. Understanding the Version Property:
    • The version property is part of Hibernate’s optimistic locking strategy. It starts at 0 and increments by 1 with each update.
    • Hibernate uses this property to check for data changes. If the database version differs from the object version, it will throw an exception to prevent lost updates (stale data).
  6. Constructor Annotations:
    • Use Lombok’s @NoArgsConstructor and @AllArgsConstructor to generate constructors.
  7. Warnings About @Data:
    • You may get a warning that @Data is not recommended for JPA entities, as it generates methods (like equals and hashCode) that may cause issues with Hibernate.
    • Refactor to explicitly define getters and setters instead.

SpringBoot JPA Test Splice @DataJpaTest

@DataJpaTest is a Spring Boot testing annotation specifically designed for testing JPA repositories. It configures only the components required for JPA-related testing, making the tests faster and more focused. Here’s what it does in detail:

Key Features of @DataJpaTest

  1. Auto-Configures JPA:
    • Configures the Spring Data JPA layer, including your EntityManager, DataSource, and Repositories.
    • Does not load the entire Spring context (e.g., controllers or services).
  2. Transactional Tests:
    • Each test is wrapped in a transaction that is rolled back at the end of the test, ensuring a clean slate for every test.
  3. In-Memory Database by Default:
    • By default, Spring Boot configures an in-memory database like H2 for testing purposes. This behavior can be overridden if a specific database configuration is provided.
  4. Scans for JPA Entities:
    • Automatically scans and configures the entities in your application.
  5. Excludes External Configurations:
    • Disables auto-configuration for non-JPA components, such as MVC, security, or web layers, to keep the tests focused and fast.

Example

Important

This is only focused at testing repositories. If we try to autowire a service or controller, it won’t work

@DataJpaTest
class BeerRepositoryTest {
 
    @Autowired
    BeerRepository beerRepository;
 
    @Test
    void testSaveBeer() {
        Beer savedBeer = beerRepository.save(Beer.builder()
                        .beerName("My Beer")
                .build());
 
        assertThat(savedBeer).isNotNull();
        assertThat(savedBeer.getId()).isNotNull();
    }
}
  1. Annotation @DataJpaTest:
    • Sets up the necessary components to test the BeerRepository in isolation.
    • Uses an in-memory database for quick and clean testing.
    • Rolls back the transaction after the test.
  2. Dependency Injection with @Autowired:
    • Injects the BeerRepository so it can be tested directly.
  3. Test Logic:
    • Creates and saves a new Beer entity using BeerRepository.
    • Validates that the savedBeer is not null and its id is not null, indicating that it was successfully persisted.

Advantages of Using @DataJpaTest

  • Focused Testing: Only the JPA layer is tested, avoiding unrelated components.
  • Fast Execution: By skipping non-relevant configurations, tests run quickly.
  • Automatic Rollback: Ensures that tests do not interfere with each other by cleaning up the database after each test.
  • Easily Mocked Database: Default use of an in-memory database simplifies setup and eliminates side effects.

This makes @DataJpaTest an ideal choice for testing Spring Data JPA repositories in isolation.

MapStruct

Using MapStruct for DTO conversion and mapping in a Spring Boot application is a clean and efficient way to automate the generation of type-safe mappers. Here’s a step-by-step guide:


1. Add MapStruct Dependency

Add the following dependencies to your pom.xml (Maven) or build.gradle (Gradle):

Maven:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version> <!-- Use the latest version -->
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.5.Final</version>
    <scope>provided</scope>
</dependency>

Gradle:

implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'

If you’re using Lombok, include the Lombok MapStruct Binding:

annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'

2. Create DTO and Entity Classes

Define the DTO and the Entity classes.

Entity:

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
 
import javax.persistence.Entity;
import javax.persistence.Id;
 
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Beer {
    @Id
    private Long id;
    private String name;
    private String type;
}

DTO:

import lombok.Data;
 
@Data
public class BeerDto {
    private Long id;
    private String name;
    private String type;
}

3. Create a Mapper Interface

Define a MapStruct mapper interface. Use the @Mapper annotation to mark it as a mapper.

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
 
@Mapper(componentModel = "spring")
public interface BeerMapper {
    
    BeerMapper INSTANCE = Mappers.getMapper(BeerMapper.class);
 
    // Entity to DTO
    BeerDto toDto(Beer beer);
 
    // DTO to Entity
    Beer toEntity(BeerDto beerDto);
}

4. Use MapStruct in a Service

Inject the mapper in your Spring Boot service to perform the conversions.

import org.springframework.stereotype.Service;
 
@Service
public class BeerService {
 
    private final BeerMapper beerMapper;
 
    public BeerService(BeerMapper beerMapper) {
        this.beerMapper = beerMapper;
    }
 
    public BeerDto convertToDto(Beer beer) {
        return beerMapper.toDto(beer);
    }
 
    public Beer convertToEntity(BeerDto beerDto) {
        return beerMapper.toEntity(beerDto);
    }
}

5. Write a Controller to Expose API

You can use the service in a REST controller to handle incoming and outgoing requests.

import org.springframework.web.bind.annotation.*;
 
@RestController
@RequestMapping("/api/beers")
public class BeerController {
 
    private final BeerService beerService;
 
    public BeerController(BeerService beerService) {
        this.beerService = beerService;
    }
 
    @PostMapping
    public BeerDto createBeer(@RequestBody BeerDto beerDto) {
        Beer beer = beerService.convertToEntity(beerDto);
        // Save beer to database (repository.save(beer))
        return beerService.convertToDto(beer);
    }
}

6. Advanced Features

Custom Field Mapping:

You can customize mappings using @Mapping or multiple mappings:

@Mapper(componentModel = "spring")
public interface BeerMapper {
    @Mapping(source = "name", target = "beerName")
    @Mapping(source = "type", target = "beerType")
    BeerDto toDto(Beer beer);
 
    @Mapping(source = "beerName", target = "name")
    @Mapping(source = "beerType", target = "type")
    Beer toEntity(BeerDto beerDto);
}

Ignore Unwanted Fields:

@Mapping(target = "someField", ignore = true)

Default Values:

@Mapping(target = "status", constant = "NEW")

7. Generate Mappers

MapStruct generates the mappers during the compilation phase. You can find the generated code in the target/generated-sources/annotations directory for Maven or build/generated/sources/annotationProcessor for Gradle.


8. Testing

Write unit tests to verify the mappings:

import static org.assertj.core.api.Assertions.assertThat;
 
public class BeerMapperTest {
 
    private final BeerMapper mapper = BeerMapper.INSTANCE;
 
    @Test
    void testEntityToDto() {
        Beer beer = Beer.builder().id(1L).name("IPA").type("Ale").build();
        BeerDto dto = mapper.toDto(beer);
 
        assertThat(dto.getId()).isEqualTo(1L);
        assertThat(dto.getName()).isEqualTo("IPA");
        assertThat(dto.getType()).isEqualTo("Ale");
    }
 
    @Test
    void testDtoToEntity() {
        BeerDto dto = new BeerDto();
        dto.setId(1L);
        dto.setName("Stout");
        dto.setType("Dark");
 
        Beer beer = mapper.toEntity(dto);
 
        assertThat(beer.getId()).isEqualTo(1L);
        assertThat(beer.getName()).isEqualTo("Stout");
        assertThat(beer.getType()).isEqualTo("Dark");
    }
}

Atomic Reference

@Override  
public Optional<BeerDTO> updateBeerById(UUID beerId, BeerDTO beer) {  
    AtomicReference<Optional<BeerDTO>> atomicReference = new AtomicReference<>();  
  
    beerRepository.findById(beerId).ifPresentOrElse(foundBeer -> {  
        foundBeer.setBeerName(beer.getBeerName());  
        foundBeer.setBeerStyle(beer.getBeerStyle());  
        foundBeer.setUpc(beer.getUpc());  
        foundBeer.setPrice(beer.getPrice());  
        foundBeer.setQuantityOnHand(beer.getQuantityOnHand());  
        atomicReference.set(Optional.of(beerMapper  
                .beerToBeerDto(beerRepository.save(foundBeer))));  
    }, () -> {  
        atomicReference.set(Optional.empty());  
    });  
  
    return atomicReference.get();  
}

The AtomicReference in this code is used as a container to hold the result of the operation within a lambda expression, ensuring it can be accessed after the lambda completes. Here’s a detailed explanation of its usage:


Understanding AtomicReference

An AtomicReference is a thread-safe wrapper for an object reference. It is commonly used when:

  1. You need to perform updates atomically.
  2. The value must be shared between multiple threads safely or modified within a closure, such as a lambda expression.

In this specific case, AtomicReference is used not because of multithreading but to work around the limitation of Java’s lambda expressions, which do not allow direct modification of variables outside their scope (i.e., effectively final variables).


What the Code Does

Context:

  • The method tries to update a beer entity in the database by its beerId.
  • If the beerId exists:
    • The fields in the entity are updated with the provided values.
    • The updated entity is saved, and its DTO is returned wrapped in Optional.
  • If the beerId does not exist:
    • An empty Optional is returned.

Role of AtomicReference:

Since variables used inside a lambda must be effectively final, a regular variable cannot be reassigned inside the ifPresentOrElse lambda. The AtomicReference acts as a mutable wrapper to hold the result (Optional<BeerDTO>), enabling updates inside the lambda.


How It Works

  1. Initialization:
    AtomicReference<Optional<BeerDTO>> atomicReference = new AtomicReference<>();

    • Creates a mutable wrapper to store the result of the operation.
  2. Lambda Expressions:
    Inside ifPresentOrElse, the atomicReference is updated based on whether the beerId exists:

    • If beerId exists:
      • Update the fields of foundBeer.
      • Save the updated entity to the database.
      • Convert the entity to a DTO and store it in the atomicReference as Optional.of(dto).
    • If beerId does not exist:
      • Set the atomicReference to Optional.empty().
  3. Final Return:
    return atomicReference.get();

    • Retrieves the result stored in atomicReference, either an updated BeerDTO wrapped in Optional or an empty Optional.

Why Not Use Regular Variables?

In Java, local variables used inside lambdas must be effectively final. This means you cannot reassign a local variable inside a lambda, but you can modify the contents of a mutable object like AtomicReference.

Without AtomicReference, the compiler would raise an error if you tried to reassign a regular variable like this:

Optional<BeerDTO> result;
beerRepository.findById(beerId).ifPresentOrElse(foundBeer -> {
    result = Optional.of(...); // Compiler error: result must be final or effectively final
}, () -> {
    result = Optional.empty(); // Compiler error
});

Using AtomicReference solves this issue because its contents can be changed even though the reference itself remains final.


Thread-Safety Consideration

Although AtomicReference is thread-safe, in this scenario, it’s primarily being used for its mutability within the lambda, not for thread-safety. If thread safety isn’t a concern, you could achieve the same behavior by using a mutable container class (e.g., a plain wrapper object) or by restructuring the code to avoid needing mutability.


Refactored Alternative (Avoiding AtomicReference)

You could refactor the code to avoid AtomicReference entirely:

@Override
public Optional<BeerDTO> updateBeerById(UUID beerId, BeerDTO beer) {
    return beerRepository.findById(beerId)
        .map(foundBeer -> {
            foundBeer.setBeerName(beer.getBeerName());
            foundBeer.setBeerStyle(beer.getBeerStyle());
            foundBeer.setUpc(beer.getUpc());
            foundBeer.setPrice(beer.getPrice());
            foundBeer.setQuantityOnHand(beer.getQuantityOnHand());
            return beerMapper.beerToBeerDto(beerRepository.save(foundBeer));
        });
}
  • If the beerId exists, the map method updates the entity and returns the DTO wrapped in Optional.
  • If the beerId does not exist, findById automatically returns an empty Optional.

This approach is more concise and avoids the need for AtomicReference.