JDK 14 Records for Spring
Join the DZone community and get the full member experience.
Join For FreeIn this article, you can see several usage cases for the JDK 14 Records.
JDK 14 Records in a Nutshell
If this is your first contact with the JDK 14 Records feature, then let's mention that Records provide a compact syntax (no boilerplate code) for declaring classes that act as plain immutable data carriers.
The best way to introduce it is via an example. Let's consider the following Java class:
xxxxxxxxxx
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;
}
public boolean equals(Object o) {
...
}
public int hashCode() {
...
}
public String toString() {
...
}
}
Starting with JDK 14, the Records syntactical sugar can be used to replace the above code with a single line of code as below:
xxxxxxxxxx
public record Author(String name, String genre) {}
That's all! Running the javap
tool on Author.class
will reveal the following code:
If we scan the properties of an immutable class then we notice that Person.class
is immutable:
- The class should be marked as
final
to suppress extensibility (other classes cannot extend this class; therefore, they cannot override methods). - All fields should be declared
private
andfinal
. (They are not visible in other classes, and they are initialized only once in the constructor of this class.) - The class should contain a parameterized
public
constructor (or aprivate
constructor and factory methods for creating instances) that initializes the fields. - The class should provide getters for fields.
- The class should not expose setters.
You can read more about immutable objects in my book, Java Coding Problems.
So, JDK 14 Records are not a replacement for mutable JavaBean classes. They cannot be used as JPA/Hibernate entities. But, they are a perfect fit for using them with Streams. They can be instantiated via the constructor with arguments and, in place of getters, we can access the fields via methods with similar names (e.g., the field
name
is exposed via thename()
method).
Further, let's see several cases of using JDK 14 Records in a Spring application.
Serializing Records as JSON
Let's consider that an author has written multiple books. Having the Author
and the Book
class, we shape this scenario by defining a List<Book>
in the Author
class:
xxxxxxxxxx
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:
xxxxxxxxxx
public record Author(String name, String genre, List<Book> books) {}
public record Book(String title, String isbn) {}
Let's consider the following data sample:
xxxxxxxxxx
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:
xxxxxxxxxx
public 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:
xxxxxxxxxx
public class BookstoreController {
private final BookstoreService bookstoreService;
public BookstoreController(BookstoreService bookstoreService) {
this.bookstoreService = bookstoreService;
}
"/authors") (
public List<Author> fetchAuthors() {
return bookstoreService.fetchAuthors();
}
}
Nevertheless, if we access the endpoint, localhost:8080/authors
, we obtain the following result:
This means that the objects cannot be serialized. The solution consists of adding the Jackson annotations, JsonProperty
:
xxxxxxxxxx
import com.fasterxml.jackson.annotation.JsonProperty;
public record Author(
"name") String name, (
"genre") String genre, (
"books") List<Book> books (
) {}
public record Book(
"title") String title, (
"isbn") String isbn (
) {}
This time, accessing the localhost:8080/authors
endpoint will produce the following JSON:
xxxxxxxxxx
[
{
"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
Let's take another look at our controller:
xxxxxxxxxx
public class BookstoreController {
private final BookstoreService bookstoreService;
public BookstoreController(BookstoreService bookstoreService) {
this.bookstoreService = bookstoreService;
}
"/authors") (
public List<Author> fetchAuthors() {
return bookstoreService.fetchAuthors();
}
}
In this controller, we use Dependency Injection for injecting a BookstoreService
instance in the controller. We could have used @Autowired
as well. But, we can try to use JDK 14 Records, as shown below:
xxxxxxxxxx
public record BookstoreController(BookstoreService bookstoreService) {
"/authors") (
public List<Author> fetchAuthors() {
return bookstoreService.fetchAuthors();
}
}
The complete code is available on GitHub.
DTOs via Records and Spring Data Query Builder
Let's set this one more time:
JDK 14 Records cannot be used with JPA/Hibernate entities. There are no setters.
Now, let's consider the following JPA entity:
xxxxxxxxxx
public class Author implements Serializable {
private static final long serialVersionUID = 1L;
strategy = GenerationType.IDENTITY) (
private Long id;
private int age;
private String name;
private String genre;
// getters and setters
}
And, the goal is to fetch a read-only list of authors containing their names and ages. For this, we need a DTO. We can define a Spring projection, a POJO, or a Java Record, as shown below:
x
public record AuthorDto(String name, int age) {}
The query that will populate the DTO can be written via Spring Data Query Builder:
xxxxxxxxxx
public interface AuthorRepository extends JpaRepository<Author, Long> {
readOnly = true) (
List<AuthorDto> findByGenre(String genre);
}
The complete application is available on GitHub.
DTOs via Records and Constructor Expression and JPQL
Having the same Author
entity and the same AuthorDto
record, we can write the query via Constructor Expression and JPQL, as follows:
xxxxxxxxxx
public interface AuthorRepository extends JpaRepository<Author, Long> {
readOnly = true) (
value = "SELECT new com.bookstore.dto.AuthorDto(a.name, a.age) FROM Author a") (
List<AuthorDto> fetchAuthors();
}
The complete application is available on GitHub.
DTOs via Records and Hibernate ResultTransformer
Sometimes, we need to fetch a DTO made of a subset of properties (columns) from a parent-child association. For such cases, we can use a SQL JOIN
that can pick up the desired columns from the involved tables. But, JOIN
returns a List<Object[]>
and most probably 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 @OneToMany
relationship between Author
and Book
entities:
xxxxxxxxxx
public class Author implements Serializable {
private static final long serialVersionUID = 1L;
strategy = GenerationType.IDENTITY) (
private Long id;
private String name;
private String genre;
private int age;
cascade = CascadeType.ALL, (
mappedBy = "author", orphanRemoval = true)
private List<Book> books = new ArrayList<>();
// getters and setters
}
xxxxxxxxxx
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
strategy = GenerationType.IDENTITY) (
private Long id;
private String title;
private String isbn;
fetch = FetchType.LAZY) (
name = "author_id") (
private Author author;
// getters and setters
}
You want to fetch the id
, name
, and age
of each author, including the id
and title
of their associated books. This time, the application relies on DTO and on the Hibernate-specific ResultTransformer
. This interface is the Hibernate-specific way to transform query results into the actual application-visible query result list. It works for JPQL and native queries and is a really powerful feature.
The first step consists of defining the DTO class. ResultTransformer
can fetch data in a DTO with a constructor and no setters or in a DTO with no constructor but with setters. Fetching the name and age in a DTO with a constructor and no setters requires a DTO that can be shaped via JDK 14 Records:
xxxxxxxxxx
import java.util.List;
public record AuthorDto(Long id, String name, int age, List books) {
public void addBook(BookDto book) {
books().add(book);
}
}
xxxxxxxxxx
public record BookDto(Long id, String title) {}
Trying to map the result set to AuthorDto
is not achievable via a built-in ResultTransformer
. You need to transform the result set from Object[]
to List<AuthorDto>
and, for this, you need the custom AuthorBookTransformer
, which represents an implementation of the ResultTransformer
interface.
This interface defines two methods — transformTuple()
and transformList()
. The transformTuple()
allows you to transform tuples, which are the elements making up each row of the query result. The transformList()
method allows you to perform the transformation on the query result as a whole:
xxxxxxxxxx
public class AuthorBookTransformer implements ResultTransformer {
private Map<Long, AuthorDto> authorsDtoMap = new HashMap<>();
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;
}
public List<AuthorDto> transformList(List list) {
return new ArrayList<>(authorsDtoMap.values());
}
}
The DAO that exploits this custom ResultTransformer
is listed below:
xxxxxxxxxx
public class Dao implements AuthorDao {
private EntityManager entityManager;
readOnly = true) (
public List<AuthorDto> fetchAuthorWithBook() {
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<AuthorDto> authors = query.getResultList();
return authors;
}
}
Finally, we can obtain the data in the following service:
xxxxxxxxxx
public class BookstoreService {
private final Dao dao;
public BookstoreService(Dao dao) {
this.dao = dao;
}
public List<AuthorDto> fetchAuthorWithBook() {
List<AuthorDto> authors = dao.fetchAuthorWithBook();
return authors;
}
}
Or, we can write the service using Records for injection, as shown below:
xxxxxxxxxx
public record BookstoreService(Dao dao) {
public List<AuthorDto> fetchAuthorWithBook() {
List<AuthorDto> authors = dao.fetchAuthorWithBook();
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.
DTO via Records, JdbcTemplate and ResultSetExtractor
Accomplishing a similar mapping via JdbcTemplate
and ResultSetExtractor
can be done as below. The AuthorDto
and BookDto
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);
}
}
xxxxxxxxxx
public record BookDto(Long id, String title) {}
x
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 more examples, I suggest you continue reading here. You may also like to explore 150+ persistence performance items for Spring Boot via Spring Boot Persistence Best Practices:
Opinions expressed by DZone contributors are their own.
Comments