Сервисы и Spring Data JPA
Экран Vaadin не должен сам ходить в базу данных. Работу с данными выносят в сервис — обычный класс Spring с аннотацией @Service. Экран получает сервис через конструктор (это называется внедрением зависимостей) и только вызывает его методы. Хранить статьи будем через Spring Data JPA — модуль Spring, который сам пишет SQL-запросы по интерфейсу.
Зависимости
Добавьте в pom.xml в блок dependencies:
<!-- 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 — база данных в памяти: она создаётся при запуске и исчезает при остановке. Для учебного проекта это удобно. Настройки — в src/main/resources/application.properties:
# src/main/resources/application.properties
spring.datasource.url=jdbc:h2:mem:blog
spring.jpa.hibernate.ddl-auto=create-dropHibernate сам создаст таблицы по классам-сущностям.
Сущность и репозиторий
// 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; }
}Геттеры и сеттеры нужны не только JPA, но и формам Vaadin: позже они будут читать и записывать поля через эти методы.
// 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 создаст реализацию сама: findAll, findById, save, delete уже есть.
Сервис
// 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);
}
}Чтобы список не был пустым, добавим три статьи при запуске:
// 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("Привет, Vaadin", "Анна", LocalDate.of(2026, 9, 1), true);
first.setContent("Vaadin позволяет собрать всю админку на Java.");
repository.save(first);
repository.save(new Post("Таблицы и формы", "Олег", LocalDate.of(2026, 9, 8), true));
repository.save(new Post("Черновик про безопасность", "Анна", LocalDate.of(2026, 9, 15), false));
}
}Сервис в экране
Удалите MainView.java из первой главы и создайте экран статей на корневом адресе:
// 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("Статьи")
public class PostListView extends VerticalLayout {
public PostListView(PostService service) {
service.findAll().forEach(post -> add(new Paragraph(post.getTitle())));
}
}На http://localhost:8080/ три строки: «Привет, Vaadin», «Таблицы и формы», «Черновик про безопасность». Spring сам передал PostService в конструктор. Если оставить MainView с тем же адресом, запуск упадёт с ошибкой «Navigation target paths ... must be unique, found navigation targets ... having the same route».
Частая ошибка
Сервис внедрён через поле с @Autowired, а используется в конструкторе:
// src/main/java/com/example/blog/views/PostListView.java (неправильно)
@Autowired
private PostService service;
public PostListView() {
service.findAll();
}Spring заполняет поля после вызова конструктора, поэтому экран падает:
java.lang.NullPointerException: Cannot invoke "com.example.blog.PostService.findAll()" because "this.service" is nullПолучайте сервис параметром конструктора, как в примере выше.