使用基于 Vaadin 的用户界面在基于 Spring Data JPA 的后端

时间:2022-12-23 18:14:06

使用基于 Vaadin 的用户界面在基于 Spring Data JPA 的后端

本指南将引导您完成构建使用基于 Vaadin 的用户界面在基于 Spring Data JPA 的后端。

您将构建什么

您将为一个简单的 JPA 存储库构建一个 Vaadin UI。您将获得一个具有完整 CRUD(创建、读取、更新和删除)功能的应用程序,以及一个使用自定义存储库方法的筛选示例。

您可以遵循以下两种不同的路径之一:

  • 从项目中已有的项目开始。initial
  • 重新开始。

本文档稍后将讨论这些差异。

你需要什么

  • 约15分钟
  • 最喜欢的文本编辑器或 IDE
  • JDK 1.8或以后
  • 格拉德尔 4+​或梅文 3.2+
  • 您也可以将代码直接导入到 IDE 中:
  • 弹簧工具套件 (STS)
  • 智能理念
  • VSCode

如何完成本指南

像大多数春天一样入门指南,您可以从头开始并完成每个步骤,也可以绕过您已经熟悉的基本设置步骤。无论哪种方式,您最终都会得到工作代码。

要从头开始,请继续从 Spring 初始化开始.

要跳过基础知识,请执行以下操作:

  • 下载​并解压缩本指南的源存储库,或使用吉特:git clone https://github.com/spring-guides/gs-crud-with-vaadin.git
  • 光盘成gs-crud-with-vaadin/initial
  • 跳转到创建后端服务.

完成后,您可以根据 中的代码检查结果。​​gs-crud-with-vaadin/complete​

从 Spring 初始化开始

你可以使用这个预初始化项目,然后单击生成以下载 ZIP 文件。此项目配置为适合本教程中的示例。

手动初始化项目:

  1. 导航到https://start.spring.io.此服务拉入应用程序所需的所有依赖项,并为您完成大部分设置。
  2. 选择 Gradle 或 Maven 以及您要使用的语言。本指南假定您选择了 Java。
  3. 单击依赖关系,然后选择 Spring Data JPA 和 H2 数据库
  4. 单击生成
  5. 下载生成的 ZIP 文件,该文件是配置了您选择的 Web 应用程序的存档。

我们将在本指南的后面部分添加 Vaadin 依赖项。

如果您的 IDE 集成了 Spring Initializr,则可以从 IDE 完成此过程。

您也可以从 Github 分叉项目,然后在 IDE 或其他编辑器中打开它。

手动初始化(可选)

如果要手动初始化项目而不是使用前面显示的链接,请按照以下步骤操作:

  1. 导航到https://start.spring.io.此服务拉入应用程序所需的所有依赖项,并为您完成大部分设置。
  2. 选择 Gradle 或 Maven 以及您要使用的语言。本指南假定您选择了 Java。
  3. 单击依赖关系,然后选择 Spring Data JPA 和 H2 数据库
  4. 单击生成
  5. 下载生成的 ZIP 文件,该文件是配置了您选择的 Web 应用程序的存档。

如果您的 IDE 集成了 Spring Initializr,则可以从 IDE 完成此过程。

创建后端服务

本指南是使用 JPA 访问数据.唯一的区别是实体类具有 getter 和 setter,并且存储库中的自定义搜索方法对最终用户来说更优雅一些。您无需阅读该指南即可完成本指南,但如果您愿意,可以。

如果从新项目开始,则需要添加实体和存储库对象。如果从项目开始,则这些对象已存在。​​initial​

以下清单(来自)定义了客户实体:​​src/main/java/com/example/crudwithvaadin/Customer.java​

package com.example.crudwithvaadin;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Customer {

@Id
@GeneratedValue
private Long id;

private String firstName;

private String lastName;

protected Customer() {
}

public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}

public Long getId() {
return id;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id,
firstName, lastName);
}

}

以下清单(来自 )定义了客户存储库:​​src/main/java/com/example/crudwithvaadin/CustomerRepository.java​

package com.example.crudwithvaadin;

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

import java.util.List;

public interface CustomerRepository extends JpaRepository<Customer, Long> {

List<Customer> findByLastNameStartsWithIgnoreCase(String lastName);
}

下面的清单(来自)显示了应用程序类,它为您创建一些数据:​​src/main/java/com/example/crudwithvaadin/CrudWithVaadinApplication.java​

package com.example.crudwithvaadin;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class CrudWithVaadinApplication {

private static final Logger log = LoggerFactory.getLogger(CrudWithVaadinApplication.class);

public static void main(String[] args) {
SpringApplication.run(CrudWithVaadinApplication.class);
}

@Bean
public CommandLineRunner loadData(CustomerRepository repository) {
return (args) -> {
// save a couple of customers
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));

// fetch all customers
log.info("Customers found with findAll():");
log.info("-------------------------------");
for (Customer customer : repository.findAll()) {
log.info(customer.toString());
}
log.info("");

// fetch an individual customer by ID
Customer customer = repository.findById(1L).get();
log.info("Customer found with findOne(1L):");
log.info("--------------------------------");
log.info(customer.toString());
log.info("");

// fetch customers by last name
log.info("Customer found with findByLastNameStartsWithIgnoreCase('Bauer'):");
log.info("--------------------------------------------");
for (Customer bauer : repository
.findByLastNameStartsWithIgnoreCase("Bauer")) {
log.info(bauer.toString());
}
log.info("");
};
}

}

瓦丁依赖关系

如果签出项目,则已设置所有必要的依赖项。但是,本节的其余部分介绍如何将Vaadin支持添加到新的Spring项目中。Spring 的 Vaadin 集成包含一个 Spring Boot 启动依赖项集合,因此您只需添加以下 Maven 代码段(或相应的 Gradle 配置):​​initial​

<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-spring-boot-starter</artifactId>
</dependency>

该示例使用比入门模块引入的默认版本更新的 Vaadin 版本。要使用较新版本,请按如下方式定义 Vaadin 物料清单 (BOM):

<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-bom</artifactId>
<version>${vaadin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

默认情况下,Gradle 不支持 BOM,但有一个方便的插件​.查看build.gradle构建文件,以获取有关如何完成相同操作的示例.

定义主视图类

主视图类(在本指南中称为)是 Vaadin UI 逻辑的入口点。在 Spring Boot 应用程序中,你只需要用它注释它,它就会被 Spring 自动拾取并显示在 Web 应用程序的根目录下。您可以通过为批注提供参数来自定义显示视图的 URL。以下列表(来自 的项目 )显示了一个简单的“Hello, World”视图:​​MainView​​​​@Route​​​​@Route​​​​initial​​​​src/main/java/com/example/crudwithvaadin/MainView.java​

package com.hello.crudwithvaadin;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;

@Route
public class MainView extends VerticalLayout {

public MainView() {
add(new Button("Click me", e -> Notification.show("Hello, Spring+Vaadin user!")));
}
}

列出数据网格中的实体

为了获得漂亮的布局,您可以使用该组件。可以使用该方法将实体列表从注入的构造函数传递到 。然后,您的正文将如下所示:​​Grid​​​​CustomerRepository​​​​Grid​​​​setItems​​​​MainView​

@Route
public class MainView extends VerticalLayout {

private final CustomerRepository repo;
final Grid<Customer> grid;

public MainView(CustomerRepository repo) {
this.repo = repo;
this.grid = new Grid<>(Customer.class);
add(grid);
listCustomers();
}

private void listCustomers() {
grid.setItems(repo.findAll());
}

}

如果具有大型表或大量并发用户,则很可能不希望将整个数据集绑定到 UI 组件。

+ 尽管 Vaadin Grid 延迟将数据从服务器加载到浏览器,但上述方法将整个数据列表保留在服务器内存中。为了节省一些内存,可以通过使用分页或使用该方法提供延迟加载数据提供程序来仅显示最顶层的结果。​​setDataProvider(DataProvider)​

筛选数据

在大型数据集成为服务器的问题之前,当用户尝试查找要编辑的相关行时,可能会让他们头疼。您可以使用组件创建筛选器条目。为此,请首先修改方法以支持筛选。以下示例(来自 中的项目)演示了如何执行此操作:​​TextField​​​​listCustomer()​​​​complete​​​​src/main/java/com/example/crudwithvaadin/MainView.java​

void listCustomers(String filterText) {
if (StringUtils.isEmpty(filterText)) {
grid.setItems(repo.findAll());
}
else {
grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
}
}


这就是Spring Data的声明式查询派上用场的地方。写入是界面中的单行定义。​​findByLastNameStartsWithIgnoringCase​​​​CustomerRepository​

可以将侦听器挂接到组件,并将其值代入该筛选器方法。作为用户类型自动调用,因为您定义了筛选器文本字段。以下示例演示如何设置此类侦听器:​​TextField​​​​ValueChangeListener​​​​ValueChangeMode.EAGER​

TextField filter = new TextField();
filter.setPlaceholder("Filter by last name");
filter.setValueChangeMode(ValueChangeMode.EAGER);
filter.addValueChangeListener(e -> listCustomers(e.getValue()));
add(filter, grid);

定义编辑器组件

由于 Vaadin UI 是纯 Java 代码,因此您可以从一开始就编写可重用的代码。为此,请为实体定义编辑器组件。您可以将其设置为Spring管理的Bean,以便可以直接将其注入编辑器并处理创建,更新和删除部分或CRUD功能。以下示例(来自 )演示了如何执行此操作:​​Customer​​​​CustomerRepository​​​​src/main/java/com/example/crudwithvaadin/CustomerEditor.java​

package com.example.crudwithvaadin;

import com.vaadin.flow.component.Key;
import com.vaadin.flow.component.KeyNotifier;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.spring.annotation.SpringComponent;
import com.vaadin.flow.spring.annotation.UIScope;
import org.springframework.beans.factory.annotation.Autowired;

/**
* A simple example to introduce building forms. As your real application is probably much
* more complicated than this example, you could re-use this form in multiple places. This
* example component is only used in MainView.
* <p>
* In a real world application you'll most likely using a common super class for all your
* forms - less code, better UX.
*/
@SpringComponent
@UIScope
public class CustomerEditor extends VerticalLayout implements KeyNotifier {

private final CustomerRepository repository;

/**
* The currently edited customer
*/
private Customer customer;

/* Fields to edit properties in Customer entity */
TextField firstName = new TextField("First name");
TextField lastName = new TextField("Last name");

/* Action buttons */
// TODO why more code?
Button save = new Button("Save", VaadinIcon.CHECK.create());
Button cancel = new Button("Cancel");
Button delete = new Button("Delete", VaadinIcon.TRASH.create());
HorizontalLayout actions = new HorizontalLayout(save, cancel, delete);

Binder<Customer> binder = new Binder<>(Customer.class);
private ChangeHandler changeHandler;

@Autowired
public CustomerEditor(CustomerRepository repository) {
this.repository = repository;

add(firstName, lastName, actions);

// bind using naming convention
binder.bindInstanceFields(this);

// Configure and style components
setSpacing(true);

save.getElement().getThemeList().add("primary");
delete.getElement().getThemeList().add("error");

addKeyPressListener(Key.ENTER, e -> save());

// wire action buttons to save, delete and reset
save.addClickListener(e -> save());
delete.addClickListener(e -> delete());
cancel.addClickListener(e -> editCustomer(customer));
setVisible(false);
}

void delete() {
repository.delete(customer);
changeHandler.onChange();
}

void save() {
repository.save(customer);
changeHandler.onChange();
}

public interface ChangeHandler {
void onChange();
}

public final void editCustomer(Customer c) {
if (c == null) {
setVisible(false);
return;
}
final boolean persisted = c.getId() != null;
if (persisted) {
// Find fresh entity for editing
customer = repository.findById(c.getId()).get();
}
else {
customer = c;
}
cancel.setVisible(persisted);

// Bind customer properties to similarly named fields
// Could also use annotation or "manual binding" or programmatically
// moving values from fields to entities before saving
binder.setBean(customer);

setVisible(true);

// Focus first name initially
firstName.focus();
}

public void setChangeHandler(ChangeHandler h) {
// ChangeHandler is notified when either save or delete
// is clicked
changeHandler = h;
}

}

在较大的应用程序中,您可以在多个位置使用此编辑器组件。另请注意,在大型应用程序中,您可能希望应用一些常见模式(如 MVP)来构建 UI 代码。

连接编辑器

在前面的步骤中,您已经了解了基于组件的编程的一些基础知识。通过使用 并将选择侦听器添加到 中,可以将编辑器完全集成到主视图中。下面的清单(来自)显示了类的最终版本:​​Button​​​​Grid​​​​src/main/java/com/example/crudwithvaadin/MainView.java​​​​MainView​

package com.example.crudwithvaadin;

import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.value.ValueChangeMode;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.annotation.UIScope;
import org.springframework.util.StringUtils;

@Route
public class MainView extends VerticalLayout {

private final CustomerRepository repo;

private final CustomerEditor editor;

final Grid<Customer> grid;

final TextField filter;

private final Button addNewBtn;

public MainView(CustomerRepository repo, CustomerEditor editor) {
this.repo = repo;
this.editor = editor;
this.grid = new Grid<>(Customer.class);
this.filter = new TextField();
this.addNewBtn = new Button("New customer", VaadinIcon.PLUS.create());

// build layout
HorizontalLayout actions = new HorizontalLayout(filter, addNewBtn);
add(actions, grid, editor);

grid.setHeight("300px");
grid.setColumns("id", "firstName", "lastName");
grid.getColumnByKey("id").setWidth("50px").setFlexGrow(0);

filter.setPlaceholder("Filter by last name");

// Hook logic to components

// Replace listing with filtered content when user changes filter
filter.setValueChangeMode(ValueChangeMode.EAGER);
filter.addValueChangeListener(e -> listCustomers(e.getValue()));

// Connect selected Customer to editor or hide if none is selected
grid.asSingleSelect().addValueChangeListener(e -> {
editor.editCustomer(e.getValue());
});

// Instantiate and edit new Customer the new button is clicked
addNewBtn.addClickListener(e -> editor.editCustomer(new Customer("", "")));

// Listen changes made by the editor, refresh data from backend
editor.setChangeHandler(() -> {
editor.setVisible(false);
listCustomers(filter.getValue());
});

// Initialize listing
listCustomers(null);
}

// tag::listCustomers[]
void listCustomers(String filterText) {
if (StringUtils.isEmpty(filterText)) {
grid.setItems(repo.findAll());
}
else {
grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
}
}
// end::listCustomers[]

}

总结

祝贺!您已经通过使用 Spring Data JPA 进行持久性编写了一个功能齐全的 CRUD UI 应用程序。而且你这样做了,没有公开任何REST服务,也不必写一行JavaScript或HTML。