Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Services and Spring Data JPA

A Vaadin view should not talk to the database itself. Data work goes into a service — an ordinary Spring class with the @Service annotation. The view receives the service through its constructor (this is called dependency injection) and only calls its methods. We will store posts with Spring Data JPA, a Spring module that writes SQL queries for you from an interface.

Dependencies

Add to the dependencies block of pom.xml:

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

H2 is an in-memory database: it is created at startup and disappears when the app stops. That is handy for a learning project. Settings go to src/main/resources/application.properties:

# src/main/resources/application.properties
spring.datasource.url=jdbc:h2:mem:blog
spring.jpa.hibernate.ddl-auto=create-drop

Hibernate creates the tables from the entity classes.

The entity and the repository

// src/main/java/com/example/blog/Post.java
package com.example.blog;
 
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.time.LocalDate;
 
@Entity
public class Post {
 
    @Id
    @GeneratedValue
    private Long id;
    private String title;
    private String author;
    private String content;
    private LocalDate publishedOn;
    private boolean published;
 
    public Post() {
    }
 
    public Post(String title, String author, LocalDate publishedOn, boolean published) {
        this.title = title;
        this.author = author;
        this.publishedOn = publishedOn;
        this.published = published;
    }
 
    public Long getId() { return id; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getAuthor() { return author; }
    public void setAuthor(String author) { this.author = author; }
    public String getContent() { return content; }
    public void setContent(String content) { this.content = content; }
    public LocalDate getPublishedOn() { return publishedOn; }
    public void setPublishedOn(LocalDate publishedOn) { this.publishedOn = publishedOn; }
    public boolean isPublished() { return published; }
    public void setPublished(boolean published) { this.published = published; }
}

Getters and setters are needed not only by JPA but also by Vaadin forms: later they will read and write the fields through these methods.

// src/main/java/com/example/blog/PostRepository.java
package com.example.blog;
 
import org.springframework.data.jpa.repository.JpaRepository;
 
public interface PostRepository extends JpaRepository<Post, Long> {
}

Spring Data creates the implementation itself: findAll, findById, save and delete are already there.

The service

// src/main/java/com/example/blog/PostService.java
package com.example.blog;
 
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Service;
 
@Service
public class PostService {
 
    private final PostRepository repository;
 
    public PostService(PostRepository repository) {
        this.repository = repository;
    }
 
    public List<Post> findAll() {
        return repository.findAll();
    }
 
    public Optional<Post> findById(Long id) {
        return repository.findById(id);
    }
 
    public Post save(Post post) {
        return repository.save(post);
    }
 
    public void delete(Post post) {
        repository.delete(post);
    }
}

So that the list is not empty, let us add three posts at startup:

// src/main/java/com/example/blog/DataLoader.java
package com.example.blog;
 
import java.time.LocalDate;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
 
@Component
public class DataLoader implements CommandLineRunner {
 
    private final PostRepository repository;
 
    public DataLoader(PostRepository repository) {
        this.repository = repository;
    }
 
    @Override
    public void run(String... args) {
        if (repository.count() > 0) {
            return;
        }
        Post first = new Post("Hello, Vaadin", "Anna", LocalDate.of(2026, 9, 1), true);
        first.setContent("Vaadin lets us build the whole admin in Java.");
        repository.save(first);
        repository.save(new Post("Grids and forms", "Oleg", LocalDate.of(2026, 9, 8), true));
        repository.save(new Post("Draft about security", "Anna", LocalDate.of(2026, 9, 15), false));
    }
}

The service in a view

Delete MainView.java from the first chapter and create a posts view at the root address:

// src/main/java/com/example/blog/views/PostListView.java
package com.example.blog.views;
 
import com.example.blog.PostService;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.PageTitle;
import com.vaadin.flow.router.Route;
 
@Route("")
@PageTitle("Posts")
public class PostListView extends VerticalLayout {
 
    public PostListView(PostService service) {
        service.findAll().forEach(post -> add(new Paragraph(post.getTitle())));
    }
}

http://localhost:8080/ shows three lines: "Hello, Vaadin", "Grids and forms", "Draft about security". Spring passed PostService to the constructor by itself. If you keep MainView at the same address, startup fails with "Navigation target paths ... must be unique, found navigation targets ... having the same route".

Common mistake

The service is injected into a field with @Autowired but used in the constructor:

// src/main/java/com/example/blog/views/PostListView.java (wrong)
@Autowired
private PostService service;
 
public PostListView() {
    service.findAll();
}

Spring fills fields after the constructor runs, so the view fails:

java.lang.NullPointerException: Cannot invoke "com.example.blog.PostService.findAll()" because "this.service" is null

Receive the service as a constructor parameter, as in the example above.

Thanks for reading!

Donate to the site — we work hard for you.