In this article, we'll explore the various scenarios where JDK 14 Records prove to be a game-changer.
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:
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.
Let’s assume that an author has penned multiple books. By defining a List
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") Listbooks ) {} 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.
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 ListfetchAuthors() { 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.
Let’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.
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.
In 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
Such an example is the below bidirectional @OneToMany 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, Listbooks) { 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
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 MapauthorsDtoMap = 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 ListretrieveAuthorWithBook() { 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 ListretrieveAuthorWithBook() { List authors = dao.retrieveAuthorWithBook(); return authors; } }
@Servicepublic record BookstoreBusinessService(DataAccessObject dao) { public ListretrieveAuthorWithBook() { 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.
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.
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:
免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。
Copyright© 2022 湘ICP备2022001581号-3