Data Persistence

This section documents the current Vaadin release line — Vaadin 24 LTS / 25.x, Java 17+, Spring Boot 3 / Jakarta EE 10 — as published at the official Vaadin documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. Flow (server-side Java) is the authoring style used throughout, with Hilla / React shown where it differs; Vaadin 7 and the pre-Flow architecture appear only as migration contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vaadin ships major releases roughly twice a year and its ecosystem iterates.

This section’s bibliography lists the reference material consulted while preparing these pages.

A Vaadin view holds no persistence code of its own — it calls a service, which uses whatever data-access stack the project already has. This page follows the Persistence guide and shows the two Vaadin-specific concerns: feeding a lazy Grid from the database, and the transaction boundary. For SQL and schema design, see the SQL Reference; for document stores, the MongoDB Reference.

Choosing a stack

Option Fits

Spring Data JPA

The default. Repositories, derived queries, Pageable — least code for CRUD.

jOOQ

Typed SQL DSL generated from the schema; full control over queries, no ORM surprises.

Flyway

Versioned schema migrations; runs on startup, independent of the query stack.

Flyway migrations live under src/main/resources/db/migration:

-- V1__create_customer.sql
CREATE TABLE customer (
    id     BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name   VARCHAR(200) NOT NULL,
    email  VARCHAR(320) NOT NULL UNIQUE
);

A lazy Grid over a repository

Extend PagingAndSortingRepository (or JpaRepository), then translate the Grid’s Query into a Spring Pageable. VaadinSpringDataHelpers does the sort mapping:

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

@Service
public class CustomerService {
    private final CustomerRepository repo;
    public CustomerService(CustomerRepository repo) { this.repo = repo; }

    public Page<Customer> list(Pageable pageable, String filter) {
        return repo.findByNameContainingIgnoreCase(
                filter == null ? "" : filter, pageable);
    }
}

// in the view
grid.setItems(
    query -> service.list(
            VaadinSpringDataHelpers.toSpringPageRequest(query),
            filterField.getValue()).stream(),
    query -> (int) service.list(
            VaadinSpringDataHelpers.toSpringPageRequest(query),
            filterField.getValue()).getTotalElements());

Now paging, sorting and filtering all execute in the database; the server never holds more than one page. See The Grid Component for the Grid side and Spring Data JPA.

jOOQ

public List<Customer> search(String term, int offset, int limit) {
    return dsl.selectFrom(CUSTOMER)
              .where(CUSTOMER.NAME.likeIgnoreCase("%" + term + "%"))
              .orderBy(CUSTOMER.NAME)
              .offset(offset).limit(limit)
              .fetchInto(Customer.class);
}

The same fetch(offset, limit) / count() pair feeds grid.setItems(…​).

The transaction boundary

A Flow view runs outside any transaction. Keep persistence work inside @Transactional service methods and return fully-initialised objects (or DTOs) — touching a lazy JPA association from the view thread throws LazyInitializationException:

@Transactional(readOnly = true)
public CustomerDetails load(Long id) {
    Customer c = repo.findById(id).orElseThrow();
    c.getOrders().size();                 // force-initialise inside the transaction
    return CustomerDetails.from(c);
}

Prefer projections / DTOs over spring.jpa.open-in-view=true, which keeps a session open for the whole request and hides the problem. A write goes through its own @Transactional method; concurrent edits are handled with a JPA @Version column (see Forms, CRUD and Master-Detail).

Refreshing after a write

The Grid does not know the database changed. After a save or delete, refresh the data view:

service.save(edited);
grid.getDataProvider().refreshAll();       // re-fetch visible rows
// or, for an in-place edit:
grid.getDataProvider().refreshItem(edited);

See also