Writing JPA applications using Java Records

In this article, we’ll explore how Java Records, available since Java 16, can be used in the context of JPA Application. We’ll uncover how Java Records, renowned for their simplicity and immutability, complement the flexibility and expressive querying abilities offered by the Criteria API.

Straight question: Can you make a JPA Entity using Java Records? Nope, you can’t. Java records are set up to be unchangeable, and they automatically deal with setting up fields. But JPA needs entities to have specific things like constructors without arguments and setters. Because of this, using records directly as JPA entities doesn’t work well—they just don’t match up. For this reason, you cannot map Database Table with a Java Record as you would do with an Entity Class.

On the other hand, a Java Record excels in transferring information between tiers where logic application is needed. Two primary use cases for Java records are:

  1. Proxied Data Application: In scenarios where data isn’t directly stored in the database but is instead transmitted to other systems after operations.
  2. Database Storage via API: Applications that store data into the database using an API such as the Criteria API.

This article particularly focuses on the latter scenario, emphasizing the advantages of the Criteria API.

Criteria API vs JPQL

In general terms, writing JPA Queries with JPQL is simpler, especially for straightforward queries or static structures.

On the other hand, the Criteria API finds its strength in several scenarios:

  • Dynamic Queries: When the query’s structure changes based on runtime conditions, the Criteria API allows programmatic query construction.
  • Type-Safety and Compile-Time Checks: By using Java code to build queries, the Criteria API reduces the risk of runtime errors, catching many issues during compile-time.
  • Complex Queries and Aggregations: For intricate queries with multiple conditions, dynamic predicates, aggregations, or nested conditions, the Criteria API’s flexibility makes it a superior choice.
  • Refactoring and Maintenance: The Criteria API offers enhanced maintainability and easier refactoring, particularly when queries evolve or require adjustments over time.

You can learn more about Criteria API in this article: How to use JPA Criteria API

Now, let’s see how we can build a JPA Application which contains a Repository Class and uses a Java Record to transfer data.

A JPA application using Java Records

This record declaration succinctly defines a simple data carrier class CustomerDTO with three properties (id, name, email) and provides the necessary methods to handle these properties, following the principles of immutability and simplicity.

public record CustomerDTO(
    Long id,
    String name,
    String email
) {}

Then, the Entity Class which contains a set of mandatory fields (matching our Java Record) and some other fields (not showed here) which are modified by other Services:

import jakarta.persistence.*;

@Entity
@Table 
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    // Other mandatory fields with @Column(nullable = false)
    // Getters and setters
}

Finally, here is the Repository Class that uses The CustomerDTO Java Record as Java Transfer Object:

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import jakarta.transaction.Transactional;
import java.util.List;

@ApplicationScoped
public class CustomerRepository {

    private final EntityManager entityManager;

    public CustomerRepository(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Transactional
    public Customer createCustomer(CustomerDTO customerDTO) {
        Customer customer = new Customer();
        customer.setName(customerDTO.name());
        customer.setEmail(customerDTO.email());

        entityManager.persist(customer);
        return customer;
    }

    public List<Customer> getAllCustomers() {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
        Root<Customer> root = cq.from(Customer.class);
        cq.select(root);

        return entityManager.createQuery(cq).getResultList();
    }

    @Transactional
    public void updateCustomer(CustomerDTO customerDTO) {
        Customer customer = entityManager.find(Customer.class, customerDTO.id());
        if (customer != null) {
            customer.setName(customerDTO.name());
            customer.setEmail(customerDTO.email());
            entityManager.merge(customer);
        }
    }
public List<Customer> getCustomersByName(CustomerDTO customerDTO) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Customer> cq = cb.createQuery(Customer.class);
        Root<Customer> root = cq.from(Customer.class);
        cq.select(root);

        Predicate namePredicate = cb.equal(root.get("name"), customerDTO.name());
        cq.where(namePredicate);

        return entityManager.createQuery(cq).getResultList();
    }

    @Transactional
    public void deleteCustomer(Long customerId) {
        Customer customer = entityManager.find(Customer.class, customerId);
        if (customer != null) {
            entityManager.remove(customer);
        }
    }
}

As you can see, the Java Record reduces the boilerplate of a standard Java Class. Besides, a Java Record by default creates immutable objects, making them inherently safer for transferring data across different parts of an application or between layers (e.g., from service to presentation layer).

Conclusion

Throughout this article, we’ve seen how using Java Records with the Criteria API makes things easier. It helps create data structures and queries in a simpler way. This teamwork helps developers make their code easier to read, cut down on unnecessary code, and make data structures clearer in JPA applications.

Found the article helpful? if so please follow us on Socials