{{announcement.body}}
{{announcement.title}}

JDK 14 Records for Spring

DZone 's Guide to

JDK 14 Records for Spring

In this article, we'll discuss several use cases for JDK 14 Records to write cleaner and more efficient code.

· Java Zone ·
Free Resource

In 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:

Java
 




xxxxxxxxxx
1
33


 
1
public final class Author {
2
  
3
  private final String name;
4
  private final String genre;    
5
  
6
  public Author(String name, String genre) {
7
    this.name = name;
8
    this.genre = genre;  
9
  }
10
 
          
11
    public String getName() {
12
        return name;
13
    }
14
 
          
15
    public String getGenre() {
16
        return genre;
17
    }
18
 
          
19
    @Override
20
    public boolean equals(Object o) {
21
      ...
22
    }
23
            
24
    @Override
25
    public int hashCode() {
26
      ...
27
    }
28
 
          
29
    @Override
30
    public String toString() {
31
      ...
32
    }
33
}



Starting with JDK 14, the Records syntactical sugar can be used to replace the above code with a single line of code as below:

Java
 




xxxxxxxxxx
1


 
1
public record Author(String name, String genre) {}



That's all! Running the javap tool on Author.class will reveal the following code:

Running javap on Author class

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 and final(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 a private 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 the  name()  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:

Java
 




xxxxxxxxxx
1
14


 
1
public final class Author {
2
  
3
  private final String name;
4
  private final String genre;
5
  private final List<Book> books;
6
  ...
7
}
8
    
9
public final Book {
10
  
11
  private String title;
12
  private String isbn;
13
  ...
14
}



If we use Records, then we can eliminate the boilerplate code as below:

Java
 




xxxxxxxxxx
1


 
1
public record Author(String name, String genre, List<Book> books) {}
2
public record Book(String title, String isbn) {}



Let's consider the following data sample:

Java
 




xxxxxxxxxx
1
10


 
1
List<Author> authors = List.of(
2
  new Author("Joana Nimar", "History", List.of(
3
    new Book("History of a day", "JN-001"),
4
    new Book("Prague history", "JN-002")
5
  )),
6
  new Author("Mark Janel", "Horror", List.of(
7
    new Book("Carrie", "MJ-001"),
8
    new Book("House of pain", "MJ-002")
9
  ))
10
);



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:

Java
 




xxxxxxxxxx
1
18


 
1
@Service
2
public class BookstoreService {
3
  
4
  public List<Author> fetchAuthors() {
5
    
6
    List<Author> authors = List.of(
7
      new Author("Joana Nimar", "History", List.of(
8
        new Book("History of a day", "JN-001"),
9
        new Book("Prague history", "JN-002")
10
      )),
11
      new Author("Mark Janel", "Horror", List.of(
12
        new Book("Carrie", "MJ-001"),
13
        new Book("House of pain", "MJ-002")
14
      )));
15
    
16
    return authors;
17
  }
18
}



And, the controller is quite simple:

Java
 




xxxxxxxxxx
1
14


 
1
@RestController
2
public class BookstoreController {
3
  
4
  private final BookstoreService bookstoreService;
5
  
6
  public BookstoreController(BookstoreService bookstoreService) {
7
    this.bookstoreService = bookstoreService;
8
  }
9
  
10
  @GetMapping("/authors")
11
  public List<Author> fetchAuthors() {
12
    return bookstoreService.fetchAuthors();
13
  }
14
}



Nevertheless, if we access the endpoint, localhost:8080/authorswe obtain the following result:

Authors endpoint

This means that the objects cannot be serialized. The solution consists of adding the Jackson annotations, JsonProperty:

Java
 




xxxxxxxxxx
1
12


 
1
import com.fasterxml.jackson.annotation.JsonProperty;
2
      
3
public record Author(
4
  @JsonProperty("name") String name, 
5
  @JsonProperty("genre") String genre, 
6
  @JsonProperty("books") List<Book> books
7
) {}
8
        
9
public record Book(
10
  @JsonProperty("title") String title, 
11
  @JsonProperty("isbn") String isbn
12
) {}



This time, accessing the localhost:8080/authors endpoint will produce the following JSON:

JSON
 




xxxxxxxxxx
1
30


 
1
[
2
  {
3
    "name": "Joana Nimar",
4
    "genre": "History",
5
    "books": [
6
      {
7
        "title": "History of a day",
8
        "isbn": "JN-001"
9
      },
10
      {
11
        "title": "Prague history",
12
        "isbn": "JN-002"
13
      }
14
    ]
15
  },
16
  {
17
    "name": "Mark Janel",
18
    "genre": "Horror",
19
    "books": [
20
      {
21
        "title": "Carrie",
22
        "isbn": "MJ-001"
23
      },
24
      {
25
        "title": "House of pain",
26
        "isbn": "MJ-002"
27
      }
28
    ]
29
  }
30
]



The complete code is available on GitHub.

Records and Dependency Injection

Let's take another look at our controller:

Java
 




xxxxxxxxxx
1
14


 
1
@RestController
2
public class BookstoreController {
3
  
4
  private final BookstoreService bookstoreService;
5
  
6
  public BookstoreController(BookstoreService bookstoreService) {
7
    this.bookstoreService = bookstoreService;
8
  }
9
  
10
  @GetMapping("/authors")
11
  public List<Author> fetchAuthors() {
12
    return bookstoreService.fetchAuthors();
13
  }
14
}



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:

Java
 




xxxxxxxxxx
1


 
1
@RestController
2
public record BookstoreController(BookstoreService bookstoreService) {
3
    
4
  @GetMapping("/authors")
5
  public List<Author> fetchAuthors() {
6
    return bookstoreService.fetchAuthors();
7
  }
8
}



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:

Java
 




xxxxxxxxxx
1
15


 
1
@Entity
2
public class Author implements Serializable {
3
  
4
  private static final long serialVersionUID = 1L;
5
  
6
  @Id
7
  @GeneratedValue(strategy = GenerationType.IDENTITY)
8
  private Long id;
9
  
10
  private int age;
11
  private String name;
12
  private String genre;
13
    
14
  // getters and setters 
15
}



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:

Java


The query that will populate the DTO can be written via Spring Data Query Builder:

Java
 




xxxxxxxxxx
1


 
1
public interface AuthorRepository extends JpaRepository<Author, Long> {
2
  
3
  @Transactional(readOnly = true)    
4
  List<AuthorDto> findByGenre(String genre);
5
}



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:

Java
 




xxxxxxxxxx
1


 
1
public interface AuthorRepository extends JpaRepository<Author, Long> {
2
  
3
  @Transactional(readOnly = true)
4
  @Query(value = "SELECT new com.bookstore.dto.AuthorDto(a.name, a.age) FROM Author a")
5
  List<AuthorDto> fetchAuthors();
6
}



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:

One to Many relationship

Java
 




xxxxxxxxxx
1
19


 
1
@Entity
2
public class Author implements Serializable {
3
  
4
  private static final long serialVersionUID = 1L;
5
  
6
  @Id
7
  @GeneratedValue(strategy = GenerationType.IDENTITY)
8
  private Long id;
9
  
10
  private String name;
11
  private String genre;
12
  private int age;
13
  
14
  @OneToMany(cascade = CascadeType.ALL,
15
             mappedBy = "author", orphanRemoval = true)
16
  private List<Book> books = new ArrayList<>();
17
     
18
  // getters and setters
19
}


Java
 




xxxxxxxxxx
1
18


 
1
@Entity
2
public class Book implements Serializable {
3
  
4
  private static final long serialVersionUID = 1L;
5
  
6
  @Id
7
  @GeneratedValue(strategy = GenerationType.IDENTITY)
8
  private Long id;
9
  
10
  private String title;
11
  private String isbn;
12
  
13
  @ManyToOne(fetch = FetchType.LAZY)
14
  @JoinColumn(name = "author_id")
15
  private Author author;
16
     
17
  // getters and setters
18
}



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:

Java
 




xxxxxxxxxx
1


 
1
import java.util.List;
2
 
          
3
public record AuthorDto(Long id, String name, int age, List books) {
4
  
5
  public void addBook(BookDto book) {
6
    books().add(book);
7
  }
8
}


Java
 




xxxxxxxxxx
1


 
1
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 ResultTransformerinterface. 

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:

Java
 




xxxxxxxxxx
1
30


 
1
public class AuthorBookTransformer implements ResultTransformer {
2
  
3
  private Map<Long, AuthorDto> authorsDtoMap = new HashMap<>();
4
  
5
  @Override
6
  public Object transformTuple(Object[] os, String[] strings) {
7
    
8
    Long authorId = ((Number) os[0]).longValue();
9
    
10
    AuthorDto authorDto = authorsDtoMap.get(authorId);
11
    
12
    if (authorDto == null) {
13
      authorDto = new AuthorDto(((Number) os[0]).longValue(), 
14
         (String) os[1], (int) os[2], new ArrayList<>());
15
    }
16
    
17
    BookDto bookDto = new BookDto(((Number) os[3]).longValue(), (String) os[4]);
18
    
19
    authorDto.addBook(bookDto);
20
    
21
    authorsDtoMap.putIfAbsent(authorDto.id(), authorDto);
22
    
23
    return authorDto;
24
  }
25
  
26
  @Override
27
  public List<AuthorDto> transformList(List list) {
28
    return new ArrayList<>(authorsDtoMap.values());
29
  }
30
}



The DAO that exploits this custom ResultTransformer is listed below:

Java
 




xxxxxxxxxx
1
22


 
1
@Repository
2
public class Dao implements AuthorDao {
3
  
4
  @PersistenceContext
5
  private EntityManager entityManager;
6
  
7
  @Override
8
  @Transactional(readOnly = true)
9
  public List<AuthorDto> fetchAuthorWithBook() {
10
    Query query = entityManager
11
      .createNativeQuery(
12
        "SELECT a.id AS author_id, a.name AS name, a.age AS age, "
13
        + "b.id AS book_id, b.title AS title "
14
        + "FROM author a JOIN book b ON a.id=b.author_id")
15
      .unwrap(org.hibernate.query.NativeQuery.class)
16
      .setResultTransformer(new AuthorBookTransformer());
17
    
18
    List<AuthorDto> authors = query.getResultList();
19
    
20
    return authors;
21
  }
22
}



Finally, we can obtain the data in the following service:

Java
 




xxxxxxxxxx
1
16


 
1
@Service
2
public class BookstoreService {
3
  
4
  private final Dao dao;
5
  
6
  public BookstoreService(Dao dao) {
7
    this.dao = dao;
8
  }
9
  
10
  public List<AuthorDto> fetchAuthorWithBook() {
11
    
12
    List<AuthorDto> authors = dao.fetchAuthorWithBook();        
13
    
14
    return authors;
15
  }
16
}



Or, we can write the service using Records for injection, as shown below:
Java
 




xxxxxxxxxx
1
10


 
1
@Service
2
public record BookstoreService(Dao dao) {
3
    
4
  public List<AuthorDto> fetchAuthorWithBook() {
5
    
6
    List<AuthorDto> authors = dao.fetchAuthorWithBook();        
7
    
8
    return authors;
9
  }
10
}



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:

Java
 




x


1
public record AuthorDto(Long id, String name, int age, List books) {
2
     
3
  public void addBook(BookDto book) {
4
    books().add(book);
5
  }
6
}


Java
 




xxxxxxxxxx
1


 
1
public record BookDto(Long id, String title) {}


Java
 




x


1
@Repository
2
@Transactional(readOnly = true)
3
public class AuthorExtractor {
4
  
5
  private final JdbcTemplate jdbcTemplate;
6
  
7
  public AuthorExtractor(JdbcTemplate jdbcTemplate) {
8
    this.jdbcTemplate = jdbcTemplate;
9
  }
10
  
11
  public List<AuthorDto> extract() {
12
    
13
    String sql = "SELECT a.id, a.name, a.age, b.id, b.title "
14
      + "FROM author a INNER JOIN book b ON a.id = b.author_id";
15
    
16
    List<AuthorDto> result = jdbcTemplate.query(sql, (ResultSet rs) -> {
17
      
18
      final Map<Long, AuthorDto> authorsMap = new HashMap<>();
19
      while (rs.next()) {
20
        Long authorId = (rs.getLong("id"));
21
        AuthorDto author = authorsMap.get(authorId);
22
           
23
        if (author == null) {
24
          author = new AuthorDto(rs.getLong("id"), rs.getString("name"),
25
            rs.getInt("age"), new ArrayList());
26
        }
27
        
28
        BookDto book = new BookDto(rs.getLong("id"), rs.getString("title"));
29
        author.addBook(book);
30
        authorsMap.putIfAbsent(author.id(), author);
31
      }
32
        
33
      return new ArrayList<>(authorsMap.values());
34
    });
35
    
36
    return result;
37
  }
38
}


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:

Java
 




x


 
1
public record Author(String name, int age) {
2
  
3
  public Author {
4
    if (age <=18 || age > 70)
5
      throw new IllegalArgumentException("...");
6
  }
7
}



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:

Topics:
java ,java14 ,records ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}