Docs

Documentation versions (currently viewingVaadin 24)

JPA Repositories

How to implement repositories with JPA and Spring Data.

Jakarta Persistence used to be called Java Persistence API, and is still often abbreviated JPA. It is a Java API for managing relational data in Java applications. Since it is an API, you can’t do much with JPA alone - you also need a JPA implementation. Hibernate is supported by Spring Boot out of the box and is therefore the implementation recommended for Vaadin applications.

The recommended way to implement JPA repositories is with Spring Data JPA. Spring Data JPA is a framework that aims to reduce the amount of boilerplate code needed to write JPA queries. It supports repositories out of the box. As long as you stick to the conventions, you don’t have to write any infrastructure code yourself.

Note
This page describes how to build repositories with JPA in Vaadin applications. It assumes you have read the Repositories documentation page. It also assumes you are already familiar with both JPA and Spring Data. If you have not used them before, you should at least read the Accessing Data with JPA guide, and have a quick look at the Spring Data JPA documentation before continuing.

Project Setup

To enable Spring Data JPA, you need to add the spring-boot-starter-data-jpa dependency to your Maven project. Add this to your POM-file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

If you are going to use the JPA Criteria API, you should enable the Hibernate Static Meta Model Generator. This is an annotation processor that generates static meta model classes based on your entities.

To enable the processor, you have to change the configuration of the Maven Compiler Plugin. Add this to your POM file:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.hibernate.orm</groupId>
                <artifactId>hibernate-jpamodelgen</artifactId>
                <version>${hibernate.version}</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

If you are using a multi-module project, you only need to make these changes in the modules that contain JPA entities.

You should also add the JDBC-driver of the database you are using.

Entities

JPA imposes some restrictions on your entities. The classes themselves cannot be final, nor can they contain any final fields. Also, they are required to have a default constructor, but this can be package private or protected.

You should add an explicit @Table annotation to the entity class, with an explicit table name.

For fields, add explicit @Column annotations, with explicit column names.

For joins, use @JoinColumn and @JoinTable, with explicit table and column names.

All this makes it easier to write Flyway migrations later.

To make it easier for Spring Data to decide whether an entity is new or persistent, you should implement the Spring Data Persistable interface, like this:

import org.springframework.data.domain.Persistable;
...

@Entity
@Table(name = "customer")
public class Customer implements Persistable<Long> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "customer_id")
    private Long id;

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public boolean isNew() {
        return id == null;
    }
}

Spring Data also provides an AbstractPersistable base class, but you shouldn’t use it. Instead, declare the @Id field directly in every entity class, or make your own base class. This gives you better control over how your entity ID:s are generated.

Override equals and hashCode so that an entity is either equal to itself, or to another entity of the same type with the same ID. If Hibernate generates the ID for you, you should consider that the ID can be null:

import org.springframework.data.domain.Persistable;
import org.springframework.data.util.ProxyUtils;
...

@Entity
@Table(name = "customer")
public class Customer implements Persistable<Long> {
    ...

// tag::snippet[]
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || !getClass().equals(ProxyUtils.getUserClass(o))) return false;
        Customer customer = (Customer) o;
        return id != null && id.equals(customer.id);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }
// end::snippet[]
}

Hibernate can return proxied versions of the entities. Because of this, you cannot directly compare the classes inside equals, as a proxied entity would not have the same class as a non-proxied one. To fix this, you can use the ProxyUtils.getUserClass utility method provided by Spring (AbstractPersistable does this as well).

To avoid accidental overwrites of data, it is recommended to use optimistic locking on all entities, like this:

import org.springframework.data.domain.Persistable;
...

@Entity
@Table(name = "customer")
public class Customer implements Persistable<Long> {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "customer_id")
    private Long id;

// tag::snippet[]
    @Version
    @Column(name = "opt_lock_version")
    private Long optLockVersion;
// end::snippet[]

    ...
}

See the Hibernate documentation for more information.

Domain Primitives

If you have domain primitives in your entities, you can handle them in two ways. Both ways have their own advantages and disadvantages.

Accessor Methods

The most straight-forward way of using domain primitives is to use the unwrapped value in the field, and convert to and from the domain primitive in the accessor methods. For example, if you have an EmailAddress domain primitive, you could do this:

@Entity
@Table(name = "customer")
public class Customer implements Persistable<Long> {
    ...

    @Column(name = "customer_email")
    private String email;

    public EmailAddress getEmail() {
        return email == null ? null : new EmailAddress(email);
    }

    public void setEmail(EmailAddress email) {
        this.email = email == null ? null : email.value();
    }
}

This approach also works with multi-value domain primitives. For example, if you have a MonetaryAmount domain primitive that consists of a BigDecimal and a CurrencyUnit enum, you could do this:

@Entity
@Table(name = "offer")
public class Offer implements Persistable<Long> {
    ...

    @Enumerated(EnumType.STRING)
    @Column(name = "currency")
    private CurrencyUnit currency;

    @Column(name = "price")
    private BigDecimal price;

    // Null-checks have been excluded for brevity

    public MonetaryAmount getPrice() {
        return new MonetaryAmount(currency, price);
    }

    public void setPrice(MonetaryAmount amount) {
        this.currency = amount.currency();
        this.price = amount.value();
    }
}

Although the accessor methods require some extra code, this approach makes it easier to write query specifications. Whenever you are doing wildcard queries, range queries, or use aggregate functions, it is much easier to work with the unwrapped types than with custom types.

Attribute Converters

You can use single-value domain primitives directly in your fields by writing attribute converters for them. For example, an attribute converter for an EmailAddress domain primitive could look like this:

import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class EmailAddressAttributeConverter implements AttributeConverter<EmailAddress, String> {

    @Override
    public String convertToDatabaseColumn(EmailAddress attribute) {
        return attribute == null ? null : attribute.value();
    }

    @Override
    public EmailAddress convertToEntityAttribute(String dbData) {
        return dbData == null ? null : new EmailAddress(dbData);
    }
}

In your entities, you could then use the converter like this:

@Entity
@Table(name = "customer")
public class Customer implements Persistable<Long> {
    ...

    @Column(name = "customer_email")
// tag::snippet[]
    @Convert(converter = EmailAddressAttributeConverter.class)
// end::snippet[]
    private EmailAddress email;

    public EmailAddress getEmail() {
        return email;
    }

    public void setEmail(EmailAddress email) {
        this.email = email;
    }
}

This approach makes your entity classes much cleaner, but has one drawback. Any query that does not check for equality becomes more difficult to write.

For example, writing a query that returns customers whose email addresses start or end with a search term would require the LIKE operator. If you are writing the query using the JPA Criteria API, the like method requires a string, not an EmailAddress. And even if it worked with EmailAddress, you might not be able to turn the search term into one. This is because the search term might only contain a part of the email address, and would fail validation.

Furthermore, attribute converters don’t work with primary keys. If you are working with domain-driven design and aggregate roots, you may want to use domain primitives for the ID:s as well. For example, you may want to use a CustomerId to refer to a customer rather than a long.

Attribute converters are a good alternative for single-value domain primitives that are not used as identifiers, and only need to be queried by equality. In all other cases, accessor methods is a better choice.

Repositories

When using Spring Data JPA, your repository interfaces should extend the Spring Data JpaRepository interface directly. For example, a repository for a Customer entity looks like this:

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

public interface CustomerRepository extends JpaRepository<Customer, Long> { // (1)

}
  1. The Long parameter is the type of the ID, or the primary key, used to identify a single customer.

You don’t have to write a class that implements the interface. Spring Data implements the repository for you during runtime, and makes the repository available for injection. For example, a customer service can use it like this:

@Service
public class CustomerService {

    private final CustomerRepository customerRepository;

    CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }
    ...
}

Spring Data repositories are persistence oriented repositories, but do on some occasions behave like collection oriented ones. This has to do with how JPA works. While an entity is managed by a persistence context, any changes made to it are automatically saved to the database when the transaction is committed. This happens regardless of whether you have called the save method or not.

When the transaction is committed or rolled back, the entities become detached. After this, any changes made to them are no longer saved to the database. For more information about entity states, see the Hibernate documentation.

Calling the save method works regardless of whether the entity is managed or detached. Therefore, you should always call the save method if you intend to save the changes. This also makes the code easier to read.

Caution
To avoid strange side effects, you should not make any changes to entities inside a transaction if you don’t intend to save them. The only way you should cancel or revert changes is by rolling back the transaction.

Query Methods

Spring Data has support for different kinds of query methods in the repository interfaces. Queries can be derived from the name of the query method, or by defining them manually in Jakarta Persistence Query Language (JPQL), or even in SQL. For details about how to do this, see the Spring Data JPA documentation.

If you are not going to use lazy loading in your Vaadin user interface, you should always put an upper limit on the size of the query result. For example, if you are using a query derived from the method name, you can add an upper limit like this:

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

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    List<Customer> findTop100ByNameContainingOrderByNameAsc(String name);
}

This method would return the first 100 customers whose names contain the given search term, and sort the result by name in ascending order.

If you need better control over the name and ordering, you can use Limit and Sort parameters, like this:

import org.springframework.data.domain.Limit;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    List<Customer> findByNameContaining(String name, Limit limit, Sort sort);
}

This allows you to specify both the limit and the sorting at runtime.

Lazy Loading

If you are going to use lazy loading in your Vaadin user interface, you should use slicing or pagination.

If you only need the entities and not the total number of entities, return a Slice, like this:

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Slice<Customer> findByNameContaining(String name, Pageable pageable);
}

A slice does not know the total number of entities in the result set. It only know whether it is the last slice or not.

If you need the total number of entities in the result set, return a Page, like this:

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Page<Customer> findByNameContaining(String name, Pageable pageable);
}

If you are using a lazy loaded Grid to show your entities, the user experience is better if it has access to the total number of entities. If this is important to you, use pagination. If you are okay with the scrollbar jumping around a little as the grid estimates the total number of entities, use slicing.

Query Specifications

Spring Data JPA supports query specifications out of the box. To enable this feature, have your repositories extend the JpaSpecificationExecutor interface, like this:

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

public interface CustomerRepository extends JpaRepository<Customer, Long>,
    JpaSpecificationExecutor<Customer> {
}

The specifications themselves are created using the JPA Criteria API. Every specification implements the Spring Data Specification interface. This is a functional interface that returns JPA predicates. Specifications can be combined in various ways using the logical operators and, or, and not.

The recommended way to write specifications is to make a utility class for every entity. For example, if you have a Customer entity, you should create a CustomerSpecification utility class. Inside this class, you should create static factory methods for every specification you support. Here is an example of a utility class with two specifications:

import org.springframework.data.jpa.domain.Specification;

public final class CustomerSpecification { // (1)

    public static Specification<Customer> emailContaining(String searchTerm) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.like(
            root.get(Customer_.EMAIL), "%" + searchTerm + "%"); // (2)
    }

    public static Specification<Customer> firstOrderDateBetween(LocalDate from, LocalDate to) {
        return (root, query, criteriaBuilder) -> criteriaBuilder.between(
            root.get(Customer_.FIRST_ORDER_DATE), from, to);
    }

    private CustomerSpecification() { // (3)
    }
}
  1. The class is final since it is not supposed to be extended.

  2. Customer_ is a static meta model class generated by Hibernate based on the Customer entity class.

  3. The class has a private constructor since it is not supposed to be instantiated.

You can then use the specifications like this:

var result = customerRepository.findAll(
        CustomerSpecification.emailContaining("acme.com")
                .and(CustomerSpecification.firstOrderDateBetween(
                        LocalDate.of(2023, 1, 31),
                        LocalDate.of(2023, 12, 31))),
        PageRequest.ofSize(10)
);
...

Spring Data has support for dynamic projections, where you specify the return type as a method parameter.

Returning only the name and ID instead of the complete entity, the earlier example would look like this:

public interface NameAndId {
    Long getId();
    String getName();
}
...
var result = customerRepository.findBy(
        CustomerSpecification.emailContaining("acme.com")
                .and(CustomerSpecification.firstOrderDateBetween(
                        LocalDate.of(2023, 1, 31),
                        LocalDate.of(2023, 12, 31))),
        query -> query.as(NameAndId.class)
                .page(PageRequest.ofSize(10))
);

You have to use interface projections with specification queries. If you want to use Java records as projections, you have to create a custom query method.

For more information about query specifications, see the Spring Data JPA documentation.

Query Objects

Spring Data query objects are interfaces that extend the Spring Data Repository interface. This is the base interface of all the other repository interfaces, and it contains no methods at all.

You write query methods for your query objects in the same way you would write query methods for your repositories. You can use projections, pagination, custom queries, and so on. However, specification queries do not work.

If you use projections, pay attention to the query method names. For example, a method named findAll always returns entities, regardless of which return type you have declared. To create a query object that returns all entities, projected onto some other type, you have to do something like this:

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.repository.Repository;

public interface ProductListItemQuery extends Repository<Product, Long> {

    Page<ProductListItem> findAllProjectedBy(Pageable pageable);

    record ProductListItem(Long productId, String name) {
    }
}

For more advanced queries, you should consider building your query objects with jOOQ. Since both jOOQ and JPA use the same data source, nothing prevents you from combining both technologies. In fact, using JPA to store and retrieve complete entities, and jOOQ for everything else has turned out to be a good combination in real-world Vaadin projects.