DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Spring Microservice Tip: Abstracting the Database Hostname With Environment Variable
  • Enterprise RIA With Spring 3, Flex 4 and GraniteDS
  • Spring Boot: How To Use Java Persistence Query Language (JPQL)
  • How To Build Web Service Using Spring Boot 2.x

Trending

  • Manual Sharding in PostgreSQL: A Step-by-Step Implementation Guide
  • Agentic AI for Automated Application Security and Vulnerability Management
  • A Complete Guide to Modern AI Developer Tools
  • 5 Subtle Indicators Your Development Environment Is Under Siege
  1. DZone
  2. Coding
  3. Java
  4. JDK 14 Records for Spring

JDK 14 Records for Spring

By 
Anghel Leonard user avatar
Anghel Leonard
DZone Core CORE ·
Apr. 29, 20 · Tutorial
Likes (7)
Comment
Save
Tweet
Share
16.9K Views

Join the DZone community and get the full member experience.

Join For Free

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/authors, we 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
x
 
1
public record AuthorDto(String name, int age) {}


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:

Database Java (programming language) Java Development Kit Spring Framework

Opinions expressed by DZone contributors are their own.

Related

  • Spring Microservice Tip: Abstracting the Database Hostname With Environment Variable
  • Enterprise RIA With Spring 3, Flex 4 and GraniteDS
  • Spring Boot: How To Use Java Persistence Query Language (JPQL)
  • How To Build Web Service Using Spring Boot 2.x

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!