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
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
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
Partner Zones AWS Cloud
by AWS Developer Relations
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
Partner Zones
AWS Cloud
by AWS Developer Relations
11 Monitoring and Observability Tools for 2023
Learn more
  1. DZone
  2. Data Engineering
  3. Data
  4. How To Best Use Java Records as DTOs in Spring Boot 3

How To Best Use Java Records as DTOs in Spring Boot 3

Explore how to best use compact Java records as DTOs for database and API calls in Spring Boot 3 with Hibernate 6 as the persistence provider.

Denis Magda user avatar by
Denis Magda
CORE ·
Feb. 25, 23 · Tutorial
Like (9)
Save
Tweet
Share
13.59K Views

Join the DZone community and get the full member experience.

Join For Free

With the Spring 6 and Spring Boot 3 releases, Java 17+ became the baseline framework version. So now is a great time to start using compact Java records as Data Transfer Objects (DTOs) for various database and API calls.

Whether you prefer reading or watching, let’s review a few approaches for using Java records as DTOs that apply to Spring Boot 3 with Hibernate 6 as the persistence provider. 


Sample Database 

Follow these instructions if you’d like to install the sample database and experiment yourself. Otherwise, feel free to skip this section:

1. Download the Chinook Database dataset (music store) for the PostgreSQL syntax.

2. Start an instance of YugabyteDB, a PostgreSQL-compliant distributed database, in Docker:

Shell
 
mkdir ~/yb_docker_data

docker network create custom-network

docker run -d --name yugabytedb_node1 --net custom-network \
  -p 7001:7000 -p 9000:9000 -p 5433:5433 \
  -v ~/yb_docker_data/node1:/home/yugabyte/yb_data --restart unless-stopped \
  yugabytedb/yugabyte:latest \
  bin/yugabyted start \
  --base_dir=/home/yugabyte/yb_data --daemon=false


3. Create the chinook database in YugabyteDB:

Shell
 
createdb -h 127.0.0.1 -p 5433 -U yugabyte -E UTF8 chinook


4. Load the sample dataset:

Shell
 
psql -h 127.0.0.1 -p 5433 -U yugabyte -f Chinook_PostgreSql_utf8.sql -d chinook


Next, create a sample Spring Boot 3 application:

1. Generate an application template using Spring Boot 3+ and Java 17+ with Spring Data JPA as a dependency.

2. Add the PostgreSQL driver to the pom.xml file:

XML
 
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
    <version>42.5.4</version>
</dependency>


3. Provide YugabyteDB connectivity settings in the application.properties file:

Properties files
 
spring.datasource.url = jdbc:postgresql://127.0.0.1:5433/chinook
spring.datasource.username = yugabyte
spring.datasource.password = yugabyte


All set! Now, you’re ready to follow the rest of the guide.

Data Model

The Chinook Database comes with many relations, but two tables will be more than enough to show how to use Java records as DTOs.

The first table is Track, and below is a definition of a corresponding JPA entity class:

Java
 
@Entity
public class Track {
    @Id
    private Integer trackId;

    @Column(nullable = false)
    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "album_id")
    private Album album;

    @Column(nullable = false)
    private Integer mediaTypeId;

    private Integer genreId;

    private String composer;

    @Column(nullable = false)
    private Integer milliseconds;

    private Integer bytes;

    @Column(nullable = false)
    private BigDecimal unitPrice;

    // Getters and setters are omitted
}


The second table is Album and has the following entity class:

Java
 
@Entity
public class Album {
    @Id
    private Integer albumId;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private Integer artistId;

    // Getters and setters are omitted
}


In addition to the entity classes, create a Java record named TrackRecord that stores short but descriptive song information:

Java
 
public record TrackRecord(String name, String album, String composer) {}

Naive Approach

Imagine you need to implement a REST endpoint that returns a short song description. The API needs to provide song and album names, as well as the author’s name. 

The previously created TrackRecord class can fit the required information. So, let’s create a record using the naive approach that gets the data via JPA Entity classes:

1. Add the following JPA Repository:

Java
 
public interface TrackRepository extends JpaRepository<Track, Integer> {
}


2. Add Spring Boot’s service-level method that creates a TrackRecord instance from the Track entity class. The latter is retrieved via the TrackRepository instance:

Java
 
@Transactional(readOnly = true)
public TrackRecord getTrackRecord(Integer trackId) {
    Track track = repository.findById(trackId).get();

    TrackRecord trackRecord = new TrackRecord(
      track.getName(),
      track.getAlbum().getTitle(),
      track.getComposer());

    return trackRecord;
}


The solution looks simple and compact, but it’s very inefficient because Hibernate needs to instantiate two entities first: Track and Album (see the track.getAlbum().getTitle()). To do this, it generates two SQL queries that request all the columns of the corresponding database tables:

SQL
 
Hibernate: 
    select
        t1_0.track_id,
        t1_0.album_id,
        t1_0.bytes,
        t1_0.composer,
        t1_0.genre_id,
        t1_0.media_type_id,
        t1_0.milliseconds,
        t1_0.name,
        t1_0.unit_price 
    from
        track t1_0 
    where
        t1_0.track_id=?
Hibernate: 
    select
        a1_0.album_id,
        a1_0.artist_id,
        a1_0.title 
    from
        album a1_0 
    where
        a1_0.album_id=?


Hibernate selects 12 columns across two tables, but TrackRecord needs only three columns! This is a waste of memory, computing, and networking resources, especially if you use distributed databases like YugabyteDB that scatters data across multiple cluster nodes.

TupleTransformer

The naive approach can be easily remediated if you query only the records the API requires and then transform a query result set to a respective Java record.

The Spring Data module of Spring Boot 3 relies on Hibernate 6. That version of Hibernate split the ResultTransformer interface into two interfaces: TupleTransformer and ResultListTransformer.

The TupleTransformer class supports Java records, so, the implementation of the public TrackRecord getTrackRecord(Integer trackId) can be optimized this way:

Java
 
@Transactional(readOnly = true)
public TrackRecord getTrackRecord(Integer trackId) {
	org.hibernate.query.Query<TrackRecord> query = entityManager.createQuery(
      """
      SELECT t.name, a.title, t.composer
      FROM Track t
      JOIN Album a ON t.album.albumId=a.albumId
      WHERE t.trackId=:id
      """).
      setParameter("id", trackId).
      unwrap(org.hibernate.query.Query.class);

      TrackRecord trackRecord = query.setTupleTransformer((tuple, aliases) -> {
        return new TrackRecord(
          (String) tuple[0],
          (String) tuple[1],
          (String) tuple[2]);
      }).getSingleResult();

    return trackRecord;
}


  • entityManager.createQuery(...) - Creates a JPA query that requests three columns that are needed for the TrackRecord class.
  • query.setTupleTransformer(...) - The TupleTransformer supports Java records, which means a TrackRecord instance can be created in the transformer’s implementation.

This approach is more efficient than the previous one because you no longer need to create entity classes and can easily construct a Java record with the TupleTransformer. Plus, Hibernate generates a single SQL request that returns only the required columns:

SQL
 
Hibernate: 
    select
        t1_0.name,
        a1_0.title,
        t1_0.composer 
    from
        track t1_0 
    join
        album a1_0 
            on t1_0.album_id=a1_0.album_id 
    where
        t1_0.track_id=?


However, there is one very visible downside to this approach: the implementation of the public TrackRecord getTrackRecord(Integer trackId) method became longer and wordier.

Java Record Within JPA Query

There are several ways to shorten the previous implementation. One is to instantiate a Java record instance within a JPA query.

First, expand the implementation of the TrackRepository interface with a custom query that creates a TrackRecord instance from requested database columns:

Java
 
public interface TrackRepository extends JpaRepository<Track, Integer> {

@Query("""
         SELECT new com.my.springboot.app.TrackRecord(t.name, a.title, t.composer)
         FROM Track t
         JOIN Album a ON t.album.albumId=a.albumId
         WHERE t.trackId=:id
         """)
TrackRecord findTrackRecord(@Param("id") Integer trackId);
}


Next, update the implementation of the  TrackRecord getTrackRecord(Integer trackId) method this way:

Java
 
@Transactional(readOnly = true)
public TrackRecord getTrackRecord(Integer trackId) {
     return repository.findTrackRecord(trackId);
}


So, the method implementation became a one-liner that gets a TrackRecord instance straight from the JPA repository: as simple as possible. 

But that’s not all. There is one more small issue. The JPA query that constructs a Java record requires you to provide a full package name for the TrackRecord class:

SQL
 
SELECT new com.my.springboot.app.TrackRecord(t.name, a.title, t.composer)...


Let’s find a way to bypass this requirement. Ideally, the Java record needs to be instantiated without the package name:

SQL
 
SELECT new TrackRecord(t.name, a.title, t.composer)...


Hypersistence Utils  

Hypersistence Utils library comes with many goodies for Spring and Hibernate. One feature allows you to create a Java record instance within a JPA query without the package name.

Let’s enable the library and this Java records-related feature in the Spring Boot application:

1. Add the library’s Maven artifact for Hibernate 6.

2. Create a custom IntegratorProvider that registers TrackRecord class with Hibernate:

Java
 
public class ClassImportIntegratorProvider implements IntegratorProvider {
    @Override
    public List<Integrator> getIntegrators() {
        return List.of(new ClassImportIntegrator(List.of(TrackRecord.class)));
    }
}


3. Update the application.properties file by adding this custom IntegratorProvider:

Properties files
 
spring.jpa.properties.hibernate.integrator_provider=com.my.springboot.app.ClassImportIntegratorProvider


After that you can update the JPA query of the TrackRepository.findTrackRecord(...) method by removing the Java record’s package name from the query string:

Java
 
@Query("""
       SELECT new TrackRecord(t.name, a.title, t.composer)
       FROM Track t
       JOIN Album a ON t.album.albumId=a.albumId
       WHERE t.trackId=:id
       """)
 TrackRecord findTrackRecord(@Param("id") Integer trackId);


It’s that simple! 

Summary

The latest versions of Java, Spring, and Hibernate have a number of significant enhancements to simplify and make coding in Java more fun. One such enhancement is built-in support for Java records that can now be easily used as DTOs in Spring Boot applications. Enjoy!

Database Java (programming language) Spring Boot Data Types

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • Key Elements of Site Reliability Engineering (SRE)
  • Introduction to Automation Testing Strategies for Microservices
  • The 5 Books You Absolutely Must Read as an Engineering Manager
  • OWASP Kubernetes Top 10

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

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

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: