Adding JPA Entities
Let’s start by creating our JPA entities.
- Create a New Package:
- Name the package
entities(you could also usedomain, but we’ll go withentitiesfor now).
- Name the package
- Add Java Classes:
- Create two classes:
BeerandCustomer. - These classes will mirror the properties of their corresponding DTOs exactly. Copy the properties from the DTOs into these classes.
- Create two classes:
- Refactoring and Annotations:
- Place the
idproperty at the top of the class for better organization (optional, but it makes the code cleaner). - Add Lombok annotations like
@Dataand@Builderto generate boilerplate code.
- Place the
- Annotate as JPA Entities:
- Mark these classes as
@Entity. - Add the
@Idannotation for theidproperty. - Add the
@Versionannotation for the version property.
- Mark these classes as
- Understanding the Version Property:
- The
versionproperty is part of Hibernate’s optimistic locking strategy. It starts at0and increments by1with 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).
- The
- Constructor Annotations:
- Use Lombok’s
@NoArgsConstructorand@AllArgsConstructorto generate constructors.
- Use Lombok’s
- Warnings About
@Data:- You may get a warning that
@Datais not recommended for JPA entities, as it generates methods (likeequalsandhashCode) that may cause issues with Hibernate. - Refactor to explicitly define
gettersandsettersinstead.
- You may get a warning that
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
- Auto-Configures JPA:
- Configures the Spring Data JPA layer, including your
EntityManager,DataSource, andRepositories. - Does not load the entire Spring context (e.g., controllers or services).
- Configures the Spring Data JPA layer, including your
- 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.
- 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.
- Scans for JPA Entities:
- Automatically scans and configures the entities in your application.
- 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
autowirea 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();
}
}- Annotation
@DataJpaTest:- Sets up the necessary components to test the
BeerRepositoryin isolation. - Uses an in-memory database for quick and clean testing.
- Rolls back the transaction after the test.
- Sets up the necessary components to test the
- Dependency Injection with
@Autowired:- Injects the
BeerRepositoryso it can be tested directly.
- Injects the
- Test Logic:
- Creates and saves a new
Beerentity usingBeerRepository. - Validates that the
savedBeeris notnulland itsidis notnull, indicating that it was successfully persisted.
- Creates and saves a new
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:
- You need to perform updates atomically.
- 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
beerIdexists:- 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
beerIddoes not exist:- An empty
Optionalis returned.
- An empty
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
-
Initialization:
AtomicReference<Optional<BeerDTO>> atomicReference = new AtomicReference<>();- Creates a mutable wrapper to store the result of the operation.
-
Lambda Expressions:
InsideifPresentOrElse, theatomicReferenceis updated based on whether thebeerIdexists:- If
beerIdexists:- Update the fields of
foundBeer. - Save the updated entity to the database.
- Convert the entity to a DTO and store it in the
atomicReferenceasOptional.of(dto).
- Update the fields of
- If
beerIddoes not exist:- Set the
atomicReferencetoOptional.empty().
- Set the
- If
-
Final Return:
return atomicReference.get();- Retrieves the result stored in
atomicReference, either an updatedBeerDTOwrapped inOptionalor an emptyOptional.
- Retrieves the result stored in
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
beerIdexists, themapmethod updates the entity and returns the DTO wrapped inOptional. - If the
beerIddoes not exist,findByIdautomatically returns an emptyOptional.
This approach is more concise and avoids the need for AtomicReference.