Unlock the Power of JDK 14 Records: 5 Real-World Examples to Boost Your Spring Development

Comments · 19 Views

In this article, we’ll delve into various scenarios where JDK 14 Records come into play.

In this article, we'll explore the various scenarios where JDK 14 Records prove to be a game-changer.
Unlocking the Power of JDK 14 Records: A Simplified Introduction

New to JDK 14 Records? Let's start with the fundamentals: Records provide a compact syntax for defining classes that serve as simple,

immutable

data holders, minus the unnecessary code.

A practical example is the best way to demonstrate this. Consider the following Java class:

public final class Author { private final String name; private final String genre; public Author(String name, String genre) { this.name = name; this.genre = genre; } public String getName() { return name; } public String getGenre() { return genre; } @Override public boolean equals(Object o) { ... } @Override public int hashCode() { ... } @Override public String toString() { ... }}Thanks to JDK 14, the Records syntax can be utilized to condense the above code into a single, concise line, making your coding experience more efficient:public record Author(String name, String genre) {}

At

t8tech.com

, we've seen firsthand how JDK 14 Records can revolutionize the way you approach Spring development. In this article, we'll delve into the various scenarios where this feature comes into play, providing you with a comprehensive understanding of its applications.
And that's the final verdict! Running the javap tool on 
Author.class

produces the following output:

Upon scrutinizing the properties of an immutable class, we notice that
Person.class indeed exhibits immutability:The class must be declared 
final

to prevent subclassing (other classes cannot extend this class, thereby precluding method overriding).All fields should be declared private

and

final. (They are inaccessible to other classes, and they are initialized solely once in the constructor of this class.)The class should feature a parameterized public constructor (or a

private

constructor and factory methods for instance creation) that initializes the fields.Executing javap on Author classThe class should provide getter methods for fields.

The class should not expose setter methods.

You can delve deeper into immutable objects in my book, Java Coding Problems.So, JDK 14 Records are not a substitute for mutable JavaBean classes. They cannot be utilized as JPA/Hibernate entities. However, they are an ideal fit for use with Streams. They can be instantiated via the constructor with arguments, and in lieu of getters, we can access the fields via methods with similar names (e.g., the field  name

is exposed via the  
    name()
  • method).Next, let’s explore several scenarios of utilizing JDK 14 Records in a Spring application. JSON Serialization of Records
  • Let’s assume that an author has penned multiple books. By defining a
  • List in the Author class, we can model this scenario, having the Author
  • and the
  • Book class:public final class Author { private final String name; private final String genre; private final List<Book> books; ...} public final Book { private String title; private String isbn; ...}If we use Records, then we can eliminate the boilerplate code as below:public record Author(String name, String genre, List<Book> books) {}public record Book(String title, String isbn) {}
  • Let’s consider the following data sample:
  • List<Author> authors = List.of( new Author("Joana Nimar", "History", List.of( new Book("History of a day", "JN-001"), new Book("Prague history", "JN-002") )), new Author("Mark Janel", "Horror", List.of( new Book("Carrie", "MJ-001"), new Book("House of pain", "MJ-002") )));
  • If we want to serialize this data as JSON via a Spring REST Controller, then most we will most likely do it, as shown below. First, we have a service that returns the data:
  • @Servicepublic class BookstoreService { public List<Author> fetchAuthors() { List<Author> authors = List.of( new Author("Joana Nimar", "History", List.of( new Book("History of a day", "JN-001"), new Book("Prague history", "JN-002") )), new Author("Mark Janel", "Horror", List.of( new Book("Carrie", "MJ-001"), new Book("House of pain", "MJ-002") ))); return authors; }}
  • And, the controller is quite simple:
@RestControllerpublic class BookstoreController { private final BookstoreService bookstoreService; public BookstoreController(BookstoreService bookstoreService) { this.bookstoreService = bookstoreService; } @GetMapping("/authors") public List<Author> fetchAuthors() { return bookstoreService.fetchAuthors(); }}

However, when we access the endpoint,

localhost:8080/authors

, we encounter the following result:This suggests that the objects are not serializable. The solution involves adding the Jackson annotations,

JsonProperty
, to facilitate serialization:

import com.fasterxml.jackson.annotation.JsonProperty; public record Author( @JsonProperty("name") String name, @JsonProperty("genre") String genre, @JsonProperty("books") List books) {} public record Book( @JsonProperty("title") String title, @JsonProperty("isbn") String isbn) {}This time, accessing the localhost:8080/authors endpoint yields the following JSON output:[ { "name": "Joana Nimar", "genre": "History", "books": [ { "title": "History of a day", "isbn": "JN-001" }, { "title": "Prague history", "isbn": "JN-002" } ] }, { "name": "Mark Janel", "genre": "Horror", "books": [ { "title": "Carrie", "isbn": "MJ-001" }, { "title": "House of pain", "isbn": "MJ-002" } ] }]

The complete code is available on GitHub.
Records and Dependency Injection: A Closer Look

Let’s revisit our controller and explore how records and dependency injection work together:

@RestControllerpublic class BookstoreController { private final BookstoreService bookstoreService; public BookstoreController(BookstoreService bookstoreService) { this.bookstoreService = bookstoreService; } @GetMapping("/authors") public List fetchAuthors() { return bookstoreService.fetchAuthors(); }}

In this controller, we utilize Dependency Injection to inject a

BookstoreService

instance. Alternatively, we could have employed @Autowired. However, we can explore the use of JDK 14 Records, as demonstrated below:@RestControllerpublic record BookstoreController(BookstoreService bookstoreService) { @GetMapping("/authors") public List<Author> fetchAuthors() { return bookstoreService.fetchAuthors(); }}The complete code is available on GitHub.DTOs via Records and Spring Data Query BuilderLet’s reiterate this crucial point:JDK 14 Records are incompatible with JPA/Hibernate entities due to the absence of setters.Now, let’s examine the following JPA entity:

@Entitypublic class Author implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private int age; private String name; private String genre; // getters and setters }
Our goal is to retrieve a read-only list of authors, including their names and ages. To achieve this, we require a DTO. We can define a Spring projection, a POJO, or a Java Record, as shown below:public record AuthorDto(String name, int age) {}The query that populates the DTO can be crafted using Spring Data Query Builder:
public interface AuthorRepository extends JpaRepository<Author, Long> { @Transactional(readOnly = true) List<AuthorDto> retrieveAuthorsByGenre(String genre);}

The complete application is available on GitHub.

DTOs via Records and Constructor Expression and JPQL
Given the same Author entity and the same 
AuthorDto

record, we can construct the query via Constructor Expression and JPQL, as follows:

public interface AuthorRepository extends JpaRepository<Author, Long> { @Transactional(readOnly = true) @Query(value = "SELECT new com.bookstore.dto.AuthorDto(a.name, a.age) FROM Author a") List<AuthorDto> retrieveAuthors();}
The comprehensive application is accessible on GitHub.DTOs via Records and Hibernate ResultTransformerIn certain scenarios, we need to retrieve a DTO comprising a subset of properties (columns) from a parent-child association. For such cases, we can utilize a SQL 
JOIN

that can extract the desired columns from the involved tables. However,

JOIN
 returns a List, and most likely, you will need to represent it as a 
List<

ParentDto

>
, where a ParentDto instance has a 
List<

ChildDto>.

Such an example is the below bidirectional

@OneToManyAuthors endpoint response relationship between

Author

and Book entities:

@Entitypublic class Author implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String genre; private int age; @OneToMany(cascade = CascadeType.ALL, mappedBy = "author", orphanRemoval = true) private List<Book> books = new ArrayList<>(); // getters and setters}
@Entitypublic class Book implements Serializable {    private static final long serialVersionUID = 1L;    @Id  @GeneratedValue(strategy = GenerationType.IDENTITY)  private Long id;    private String title;  private String isbn;    @ManyToOne(fetch = FetchType.LAZY)  @JoinColumn(name = "author_id")  private Author author;       // getters and setters}To retrieve the id
,

name, and age

of each author, along with the
id and title
of their associated books, the application leverages DTO and the Hibernate-specific

ResultTransformer

. This interface enables the transformation of query results into the actual application-visible query result list, supporting both JPQL and native queries, and is a remarkably powerful feature. 

The initial step involves defining the DTO class. The

ResultTransformer

can fetch data in a DTO with a constructor and no setters or in a DTO with no constructor but with setters. To fetch the name and age in a DTO with a constructor and no setters, a DTO shaped via JDK 14 Records is required:

import java.util.List;public record AuthorDto(Long id, String name, int age, List books) { public void addBook(BookDto book) { books().add(book); }}
public record BookDto(Long id, String title) {}However, assigning the result set to AuthorDto
is not feasible through a native

ResultTransformer. Instead, you need to convert the result set from Object[] to List

, which necessitates the custom
AuthorBookTransformer, an implementation of the ResultTransformer
interface. 

This interface specifies two methods —

transformTuple()

and

transformList()

. The

transformTuple()
method facilitates the transformation of tuples, which constitute each row of the query result. The

transformList()

method, on the other hand, enables you to perform the transformation on the query result as a whole:
public class AuthorBookTransformer implements ResultTransformer { private Map authorsDtoMap = new HashMap<>(); @Override public Object transformTuple(Object[] os, String[] strings) { Long authorId = ((Number) os[0]).longValue(); AuthorDto authorDto = authorsDtoMap.get(authorId); if (authorDto == null) { authorDto = new AuthorDto(((Number) os[0]).longValue(), (String) os[1], (int) os[2], new ArrayList<>()); } BookDto bookDto = new BookDto(((Number) os[3]).longValue(), (String) os[4]); authorDto.addBook(bookDto); authorsDtoMap.putIfAbsent(authorDto.id(), authorDto); return authorDto; } @Override public List transformList(List list) { return new ArrayList<>(authorsDtoMap.values()); }}

The bespoke DAO implementation that leverages this custom

ResultTransformer
 is presented below:@Repositorypublic class DataAccessObject implements AuthorDataAccessObject {    @PersistenceContext  private EntityManager entityManager;    @Override  @Transactional(readOnly = true)  public List retrieveAuthorWithBook() {    Query query = entityManager      .createNativeQuery(        "SELECT a.id AS author_id, a.name AS name, a.age AS age, "        + "b.id AS book_id, b.title AS title "        + "FROM author a JOIN book b ON a.id=b.author_id")      .unwrap(org.hibernate.query.NativeQuery.class)      .setResultTransformer(new AuthorBookTransformer());        List authors = query.getResultList();        return authors;  }}In the end, we can obtain the data in the following service:
@Servicepublic class BookstoreBusinessService { private final DataAccessObject dao; public BookstoreBusinessService(DataAccessObject dao) { this.dao = dao; } public List retrieveAuthorWithBook() { List authors = dao.retrieveAuthorWithBook(); return authors; }}

@Servicepublic record BookstoreBusinessService(DataAccessObject dao) { public List retrieveAuthorWithBook() { List authors = dao.retrieveAuthorWithBook(); return authors; }}

The complete application is available on GitHub.
Starting with Hibernate 5.2,  ResultTransformer is deprecated. Until a replacement is available (in Hibernate 6.0), it can be used.Read further here.
Data Transfer Objects via Records, JdbcTemplate, and ResultSetExtractor

Achieving a similar mapping via  

JdbcTemplate 
and  ResultSetExtractor can be accomplished as follows. The  
AuthorDataTransferObject 

and  

BookDataTransferObject 

are the same from the previous section:

public record AuthorDto(Long id, String name, int age, List books) { public void addBook(BookDto book) { books().add(book); }}

public record BookDto(Long id, String title) {}@Repository@Transactional(readOnly = true)public class AuthorExtractor { private final JdbcTemplate jdbcTemplate; public AuthorExtractor(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } public List<AuthorDto> extract() { String sql = "SELECT a.id, a.name, a.age, b.id, b.title " + "FROM author a INNER JOIN book b ON a.id = b.author_id"; List<AuthorDto> result = jdbcTemplate.query(sql, (ResultSet rs) -> { final Map<Long, AuthorDto> authorsMap = new HashMap<>(); while (rs.next()) { Long authorId = (rs.getLong("id")); AuthorDto author = authorsMap.get(authorId); if (author == null) { author = new AuthorDto(rs.getLong("id"), rs.getString("name"), rs.getInt("age"), new ArrayList()); } BookDto book = new BookDto(rs.getLong("id"), rs.getString("title")); author.addBook(book); authorsMap.putIfAbsent(author.id(), author); } return new ArrayList<>(authorsMap.values()); }); return result; }}The complete application is available on GitHub.Refine the Implementation Java Records allow us to validate the arguments of the constructor, therefore the following code is ok:

public record Author(String name, int age) { public Author { if (age <=18 || age > 70) throw new IllegalArgumentException("..."); }}
For a deeper understanding, I suggest exploring this valuable resource. Furthermore, you may also find it advantageous to examine over 150 key considerations for optimizing persistence performance in Spring Boot, as detailed in Spring Boot Persistence Best Practices:			    
Comments