spring

Using Java Records with Spring Data JPA

Java Records have become a valuable feature in modern Java development, introduced in Java 16. They simplify the creation of classes, especially for handling simple data. However, when it comes to using Java Records as JPA (Java Persistence API) entities in a Spring Data JPA project, there are some challenges to overcome. In this article, we’ll delve into these challenges and explore how to effectively utilize Java Records with Spring Data JPA.

1. Challenges of Using Java Records as JPA Entities

1.1 Lack of Default Constructor

In the context of JPA, entities need to have a default, no-argument constructor. This is a special constructor that takes no parameters and is used by JPA for various operations. However, by default, Java Records provides a constructor that initializes all its components, and this constructor is not a no-argument one.

Example:
Let’s consider a simple Java Record representing a Track:

package org.example;


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

}

1.2 Immutability

Java Records are designed to be immutable, meaning their properties cannot be changed after they are set. However, JPA requires entities to have mutable properties, as it needs to update and persist data. This contrast between immutability and mutability can lead to conflicts when using Java Records as JPA entities.

1.3 Inheritance

Handling inheritance with Java Records can be challenging, especially when working with JPA’s various inheritance strategies like Single Table Inheritance, Joined Inheritance, or Table Per Class. Java Records inherently have final fields, which might not align well with JPA’s inheritance mechanisms.

2. How Can We Utilize Records in JPA?

Records however can only be used as projections. Popular JPA implementations like Hibernate depend upon entities that have no argument constructors, non-final fields, setters, and non-final classes, for the creation of proxies, all of which are either discouraged or explicitly prevented by records.

Let’s dive into some code examples demonstrating the usage of Java Records alongside with the JPA in Spring Boot 3 Applications.

3. Demo Project Setup

To illustrate how to use Java Records with Spring Data JPA, let’s set up a simple project. In this project, we’ll define a Track entity to perform basic CRUD (Create, Read, Update, Delete) operations on it, and a TrackRecord

3.1 Dependencies

To create our project, we’ll need the following dependencies:

  • Spring Boot Starter Data JPA: This provides Spring Data JPA functionality.
  • H2 Database: We’ll use an H2 in-memory database for simplicity.

3.2 Entity Definition

We’ll start by defining our Track and Album entities.

Example:

package org.example;

import jakarta.persistence.*;

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

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

    public Integer getTrackId() {
        return trackId;
    }

    public void setTrackId(Integer trackId) {
        this.trackId = trackId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Album getAlbum() {
        return album;
    }

    public void setAlbum(Album album) {
        this.album = album;
    }

    public String getComposer() {
        return composer;
    }

    public void setComposer(String composer) {
        this.composer = composer;
    }
}
package org.example;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;

@Entity
public class Album {
    @Id
    private Integer albumId;
    private String title;
    private String artistid;

    public Integer getAlbumId() {
        return albumId;
    }

    public void setAlbumId(Integer albumId) {
        this.albumId = albumId;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getArtistid() {
        return artistid;
    }

    public void setArtistid(String artistid) {
        this.artistid = artistid;
    }
}

3.3 Spring Data Repository

Next, we’ll create a Spring Data JPA repository for the Track entity. This repository interface will handle database operations related to the Track entity.

Example:

package org.example;

import org.springframework.data.jpa.repository.JpaRepository;

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

By extending JpaRepository, we gain access to various methods for performing common database operations like saving, retrieving, updating, and deleting Track Entities.

4. Naive Approach

This way is a simple one and less optimal as it asks the database to retrieve the whole Track.

package org.example;

import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TrackService {
    @Autowired
    TrackRepository repository;

    @Autowired
    EntityManager entityManager;
    @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;
    }
}

This code is a Java class named TrackService that provides a method called getTrackRecord which retrieves a TrackRecord object based on a given trackId.

Here are the main components of the code:

  • The TrackService class is annotated with @Service, indicating that it is a service component managed by the Spring framework.
  • The TrackRepository and EntityManager are autowired into the class, which means that Spring will automatically inject instances of these dependencies.
  • The getTrackRecord method is annotated with @Transactional(readOnly = true), indicating that the method is read-only and operates within a transactional context.
  • Inside the method, it retrieves a Track object from the TrackRepository based on the given trackId.
  • It then creates a TrackRecord object using the properties of the retrieved Track object.
  • Finally, it returns the TrackRecord object.

This code follows the Spring framework conventions for creating a service class that interacts with a repository and performs database operations.

Fig. 1: Naive Approach Output Result.
Fig. 1: Naive Approach Output Result.

5. Tuple Transformers

We will use Tuple transformers for this example. We are going to use an Entity Manager and create a JPA query in which we can SELECT only the entities we want. In this case we will join the Track entity with the Album Entity.

To use the tuple transformer all we need to do is define it in our query instance as shown in the code snippet below.

package org.example;

import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TrackService {
    @Autowired
    TrackRepository repository;

    @Autowired
    EntityManager entityManager;
  
    @Transactional (readOnly = true)
    public TrackRecord getTrackRecord1(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;
    }
}

This code is a Java class named TrackService that provides two methods for retrieving TrackRecord objects based on a given trackId. Here is a breakdown of the code:

  • The TrackService class is annotated with @Service, indicating that it is a service component managed by the Spring framework.
  • The TrackRepository and EntityManager are autowired into the class, which means that Spring will automatically inject instances of these dependencies.
  • The method, getTrackRecord1, is also annotated with @Transactional(readOnly = true).
    • Inside the method, it creates a Hibernate query using the entityManager.createQuery method.
    • The query selects the nametitle, and composer properties from the Track entity and the Album entity, joined on their respective foreign key relationships.
    • The trackId parameter is set as a named parameter in the query.
    • The setTupleTransformer method is used to transform the query result tuple into a TrackRecord object.
    • Finally, it executes the query and retrieves the single result as a TrackRecord object.

This code demonstrates the usage of Spring’s transactional management and the integration of JPA (Java Persistence API) with Hibernate for database operations.

Fig. 2: Using Tuple Transformers Output.
Fig. 2: Using Tuple Transformers Output.

6. Custom Query

This time we will define a custom query in our TrackRepository. Before we had an empty class but now we will add our own method.

package org.example;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.data.jpa.repository.Query;

public interface TrackRepository extends JpaRepository<Track, Integer> {

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

}

This way we use a smaller implementation than the previous one. We tell the JpaRepository how we would like to interact with our database. This way we can be very specific so we don’t waste our database computational resources, and the code it’s much cleaner and readable.

Now we just have to call the new method in our TrackService class.

@Transactional(readOnly = true)
    public TrackRecord getTrackRecord2(Integer trackId) {
        return repository.findTrackRecord(trackId);
    }
Fig. 3: Using Custom Query Method.
Fig. 3: Using Custom Query Method.

7. Conclusion

Using Java Records as JPA entities in a Spring Data JPA project is certainly possible, but it does come with some challenges. By addressing issues such as the lack of default constructors, immutability, and inheritance, you can harness the benefits of Java Records while still benefiting from the power of JPA and Spring Data.

In this article, we’ve explored the challenges and best practices of using Java Records with Spring Data JPA, along with a demo project to illustrate these concepts. When used wisely, Java Records can simplify your code and enhance readability in your JPA-based applications. As a beginner in coding, it’s important to practice and experiment with these concepts to gain a deeper understanding of how they work in real-world scenarios.

8. Download the Source Code

This was an example of how we can implement Java Records in our Spring Boot 3 Application using the JPA Repository.

Download
You can download the full source code of this example here: Using Java Records with Spring Data JPA

Odysseas Mourtzoukos

Mourtzoukos Odysseas is studying to become a software engineer, at Harokopio University of Athens. Along with his studies, he is getting involved with different projects on gaming development and web applications. He is looking forward to sharing his knowledge and experience with the world.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button