> Markdown version of [JPA & Spring Data](https://vaadin.com/docs/next/building-apps/forms-data/persistence/spring-data). Section index: [llms.txt](https://vaadin.com/docs/next/building-apps/llms.txt)

# JPA & Spring Data

Jakarta Persistence (formerly Java Persistence API, still abbreviated JPA) is a Java API for managing relational data. It maps Java objects to database tables and tracks changes automatically. [Hibernate](https://hibernate.org/) is the JPA implementation supported by Spring Boot and recommended for Vaadin applications.

[Spring Data JPA](https://spring.io/projects/spring-data-jpa) builds on JPA to reduce boilerplate. It generates repository implementations at runtime based on interface definitions.

JPA is well suited for [aggregate-oriented persistence](https://vaadin.com/docs/next/building-apps/forms-data/persistence.md)—loading object graphs, working with entities that have lifecycles and business rules, and saving them back. If you need fine-grained control over SQL, complex reporting queries, or bulk operations, consider combining JPA with [jOOQ](https://vaadin.com/docs/next/building-apps/forms-data/persistence/jooq.md) or using jOOQ alone.

> **Note:** This page assumes you’ve already [added Spring Data JPA](https://vaadin.com/docs/next/building-apps/forms-data/persistence/add-spring-data.md) to your project and are familiar with JPA concepts. If you’re new to JPA, read the [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa) guide first.

## <a id="entities"></a>Entities

JPA entities represent your domain objects and their lifecycles. An `Order` entity might transition through states, enforce pricing rules, and maintain consistency with its line items.

### <a id="constraints"></a>Constraints

JPA imposes some restrictions on entity classes:

- Classes cannot be `final`

- Fields cannot be `final`

- A default (parameter-less) constructor is required, but can be `protected` or package-private

### <a id="naming"></a>Naming

JPA deduces table and column names from class and field names. To make [Flyway migrations](https://vaadin.com/docs/next/building-apps/forms-data/persistence/add-flyway.md) easier to write, explicitly declare names:

- `@Table` on entity classes

- `@Column` on fields

- `@JoinColumn` and `@JoinTable` on associations

### <a id="identity"></a>Identity

Spring Data checks the `@Id` field to decide whether an entity is new. If the ID is `null`, the entity is new; otherwise, it’s persistent.

> **Note:** If you assign IDs before persisting, implement the Spring Data `Persistable` interface.

Don’t use Spring Data’s `AbstractPersistable` base class. Declare the `@Id` field directly in each entity, or create your own base class. This gives you better control over ID generation.

### <a id="equality"></a>Equality

Override `equals` and `hashCode` so that an entity equals itself or another entity of the same type with the same ID. Handle the case where the ID is `null` (before persistence):

```java
import jakarta.persistence.*;
import org.springframework.data.util.ProxyUtils;

@Entity
@Table(name = "customer")
public class Customer {

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || !getClass().equals(ProxyUtils.getUserClass(o))) return false; // (1)
        Customer other = (Customer) o;
        return id != null && id.equals(other.id);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id);
    }
}
```

1. Use `ProxyUtils.getUserClass` because Hibernate may return proxied entities whose classes don’t match directly.

## <a id="domain-primitives"></a>Domain Primitives

[Domain primitives](https://vaadin.com/docs/next/building-apps/forms-data/consistency/domain-primitives.md) can be integrated with JPA entities in several ways.

### <a id="accessor-methods"></a>Accessor Methods

Use the unwrapped value in the field, converting in accessor methods:

```java
@Entity
@Table(name = "customer")
public class Customer {

    @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 also works for multi-value domain primitives:

```java
@Entity
@Table(name = "offer")
public class Offer {

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

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

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

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

This approach makes query specifications easier to write since you work with unwrapped types.

### <a id="attribute-converters"></a>Attribute Converters

For single-value domain primitives, write an attribute converter:

```java
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class EmailAddressConverter 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);
    }
}
```

Then apply it:

```java
@Column(name = "customer_email")
@Convert(converter = EmailAddressConverter.class)
private EmailAddress email;
```

This is cleaner but makes non-equality queries harder—`LIKE` queries require strings, not domain primitives. Attribute converters also don’t work with primary keys.

Use converters for domain primitives that are only queried by equality and aren’t identifiers.

### <a id="embeddable-records"></a>Embeddable Records

Multi-value domain primitives can be `@Embeddable`. Hibernate 6.2+ supports Java records:

```java
@Embeddable
public record MonetaryAmount(
    @Enumerated(EnumType.STRING) CurrencyUnit currency,
    BigDecimal value
) {
    public MonetaryAmount {
        requireNonNull(currency);
        requireNonNull(value);
    }
}
```

Used in an entity:

```java
@Embedded
@AttributeOverrides({
    @AttributeOverride(name = "currency", column = @Column(name = "unit_price_currency")),
    @AttributeOverride(name = "value", column = @Column(name = "unit_price"))
})
private MonetaryAmount unitPrice;
```

Non-record embeddables must be non-final with a parameter-less constructor, which conflicts with domain primitive immutability requirements.

## <a id="repositories"></a>Repositories

Repository interfaces extend `JpaRepository`:

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

public interface OrderRepository extends JpaRepository<Order, Long> {
}
```

Spring Data implements the interface at runtime. Inject it like any Spring bean:

```java
@Service
public class OrderService {

    private final OrderRepository orderRepository;

    OrderService(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }
}
```

### <a id="entity-state-and-saving"></a>Entity State and Saving

JPA entities can be **managed** or **detached**. While managed (within a transaction), changes are automatically saved when the transaction commits—even without calling `save`.

Once the transaction completes, entities become detached. Further changes aren’t persisted without an explicit `save`.

```java
@Transactional
public void updateCustomer(Long id, String newName) {
    var customer = customerRepository.findById(id).orElseThrow();
    customer.setName(newName);
    // Changes persist automatically at transaction commit
}
```

For clarity, call `save` explicitly when you intend to persist changes:

```java
customer.setName(newName);
customerRepository.save(customer);
```

Calling `save` doesn’t necessarily write the changes to the database right away. JPA collects them, and *flushes* them to the database when the transaction commits, or earlier if a query needs them. Use `saveAndFlush` when you need the changes written before the method returns. For example, the database or JPA may assign values on write, such as an incremented `@Version` field or an update timestamp. If you map the entity to a DTO inside the transaction, flush first so that the DTO contains these values. Flushing also makes constraint violations surface from the `saveAndFlush` call instead of at commit. In other cases, `save` is enough.

> **Caution:** Don’t modify managed entities if you don’t intend to save the changes. Roll back the transaction to discard modifications.

See [Transactions](https://vaadin.com/docs/next/building-apps/forms-data/consistency/transactions.md) for more on transaction management.

## <a id="locking"></a>Locking

JPA supports both [optimistic](https://vaadin.com/docs/next/building-apps/forms-data/consistency/optimistic-locking.md) and [pessimistic locking](https://vaadin.com/docs/next/building-apps/forms-data/consistency/pessimistic-locking.md).

### <a id="optimistic-locking"></a>Optimistic Locking

Add a version field to detect concurrent modifications:

```java
@Entity
@Table(name = "customer")
public class Customer {

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

    @Version
    @Column(name = "_version")
    private Long version;
}
```

If two transactions load the same entity and both try to save, the second fails with an optimistic locking exception.

### <a id="pessimistic-locking"></a>Pessimistic Locking

Use `@Lock` on query methods to acquire database locks:

```java
public interface AccountRepository extends JpaRepository<Account, Long> {

    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("select a from Account a where a.id = :accountId")
    Account lockForWriting(Long accountId);
}
```

See the [Hibernate documentation](https://docs.jboss.org/hibernate/orm/current/introduction/html_single/Hibernate_Introduction.html#optimistic-and-pessimistic-locking) for details.

## <a id="query-methods"></a>Query Methods

Spring Data derives queries from method names:

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

Always limit results to avoid memory issues:

```java
List<Customer> findTop100ByNameContainingOrderByNameAsc(String name);
```

Or use parameters:

```java
List<Customer> findByNameContaining(String name, Limit limit, Sort sort);
```

### <a id="pagination"></a>Pagination

For large datasets, use pagination:

```java
Page<Customer> findByNameContaining(String name, Pageable pageable);
```

A `Page` includes total count (requires an extra query). Use `Slice` if you only need to know whether more results exist:

```java
Slice<Customer> findByNameContaining(String name, Pageable pageable);
```

## <a id="query-specifications"></a>Query Specifications

For dynamic queries, extend `JpaSpecificationExecutor`:

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

Create specification factory methods:

```java
public final class CustomerSpecifications {

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

    public static Specification<Customer> createdBetween(LocalDate from, LocalDate to) {
        return (root, query, cb) -> cb.between(root.get(Customer_.CREATED_DATE), from, to);
    }

    private CustomerSpecifications() {}
}
```

Combine them:

```java
var customers = customerRepository.findAll(
    emailContaining("acme.com").and(createdBetween(startDate, endDate)),
    PageRequest.ofSize(20)
);
```

## <a id="query-classes"></a>Query Classes

Not every query returns complete entities. List views often need only a few fields. Reports aggregate across tables. These are [table-oriented operations](https://vaadin.com/docs/next/building-apps/forms-data/persistence.md)—data transformations rather than entity lifecycle management.

For such queries, create separate query interfaces:

```java
public interface OrderSummaryQuery extends Repository<Order, Long> {

    @Query("""
        SELECT new com.example.OrderSummary(o.id, o.status, o.total, c.name)
        FROM Order o JOIN o.customer c
        WHERE o.createdDate >= :since
        """)
    Page<OrderSummary> findRecentOrders(@Param("since") LocalDate since, Pageable pageable);

    record OrderSummary(Long orderId, OrderStatus status, BigDecimal total, String customerName) {}
}
```

For complex reporting queries, consider [jOOQ](https://vaadin.com/docs/next/building-apps/forms-data/persistence/jooq.md) instead. JPA and jOOQ work well together—use JPA for entity persistence and jOOQ for queries.

## <a id="entities-in-flow-views"></a>Entities in Flow Views

A Flow view keeps its state in server memory between requests, as explained in [Where State Lives](https://vaadin.com/docs/next/building-apps/architecture/runtime-model.md#where-state-lives). An entity that a view holds is detached: the transaction that loaded it has ended, and nothing more can be loaded through it.

### <a id="loading-what-the-view-needs"></a>Loading What the View Needs

When code accesses a lazy association that wasn’t loaded inside the transaction, Hibernate throws a `LazyInitializationException`. In a view, this happens in listeners, in later requests, and in background threads alike.

Spring Boot enables the `spring.jpa.open-in-view` property by default, and logs a warning about it at startup. The property keeps an entity manager open while Spring MVC handles a request, so that lazy associations can still be loaded. It has no effect on the requests that Vaadin handles, so it doesn’t make lazy loading work in Flow views. It does affect REST controllers in the same application. Turn it off, so that all of your code follows the same rules:

```properties
spring.jpa.open-in-view=false
```

Instead, have the application service load everything the view needs, inside its transaction. For a view that shows a single aggregate, such as a form, fetch the associations that the view uses, for example with an entity graph:

```java
public interface OrderRepository extends JpaRepository<Order, Long> {

    @EntityGraph(attributePaths = {"customer"})
    Optional<Order> findWithCustomerById(Long id);
}
```

For a grid or a list, use a [query class](#query-classes) that returns DTOs with only the columns the view shows. A DTO has no lazy associations to trip over.

Don’t work around the exception by accessing each association inside the transaction, for example while mapping the entities to DTOs. That runs one more query for each row, which is known as the N+1 problem. A query with a fetch join or an entity graph loads the same data in a single query. See [Association fetching](https://docs.hibernate.org/orm/current/introduction/html_single/#association-fetching) in the Hibernate documentation for the options.

### <a id="keeping-what-the-view-holds-small"></a>Keeping What the View Holds Small

Everything that a view holds stays in memory for as long as the UI exists, and with session replication, it also has to be serializable. An entity brings along everything that was loaded with it. A grid that is given a list of 10,000 entities keeps all of them, whereas a [paginated grid](https://vaadin.com/docs/next/building-apps/forms-data/add-grid/paginated-data.md) keeps only the rows that the user has scrolled to. To find out what your views cost, [measure the memory per session](https://vaadin.com/docs/next/building-apps/architecture/capacity-and-failover.md#measure-memory-per-session).

Keep only what the view shows or edits: DTOs for grids, the FDO for a form, and IDs for items the view has to find again later. [Input & Output](https://vaadin.com/docs/next/building-apps/business-logic/add-service.md#input-output) compares returning entities and DTOs from application services.
