本指南将引导您完成构建使用基于 Vaadin 的用户界面在基于 Spring Data JPA 的后端。
【资料图】
您将为一个简单的 JPA 存储库构建一个 Vaadin UI。您将获得一个具有完整 CRUD(创建、读取、更新和删除)功能的应用程序,以及一个使用自定义存储库方法的筛选示例。
您可以遵循以下两种不同的路径之一:
从项目中已有的项目开始。initial
重新开始。本文档稍后将讨论这些差异。
像大多数春天一样入门指南,您可以从头开始并完成每个步骤,也可以绕过您已经熟悉的基本设置步骤。无论哪种方式,您最终都会得到工作代码。
要从头开始,请继续从 Spring 初始化开始.
要跳过基础知识,请执行以下操作:
下载并解压缩本指南的源存储库,或使用吉特:git clonehttps://github.com/spring-guides/gs-crud-with-vaadin.git
光盘成gs-crud-with-vaadin/initial
跳转到创建后端服务.完成后,您可以根据 中的代码检查结果。gs-crud-with-vaadin/complete
你可以使用这个预初始化项目,然后单击生成以下载 ZIP 文件。此项目配置为适合本教程中的示例。
手动初始化项目:
导航到https://start.spring.io.此服务拉入应用程序所需的所有依赖项,并为您完成大部分设置。选择 Gradle 或 Maven 以及您要使用的语言。本指南假定您选择了 Java。单击依赖关系,然后选择Spring Data JPA和H2 数据库。单击生成。下载生成的 ZIP 文件,该文件是配置了您选择的 Web 应用程序的存档。我们将在本指南的后面部分添加 Vaadin 依赖项。 |
如果您的 IDE 集成了 Spring Initializr,则可以从 IDE 完成此过程。 |
您也可以从 Github 分叉项目,然后在 IDE 或其他编辑器中打开它。 |
如果要手动初始化项目而不是使用前面显示的链接,请按照以下步骤操作:
导航到https://start.spring.io.此服务拉入应用程序所需的所有依赖项,并为您完成大部分设置。选择 Gradle 或 Maven 以及您要使用的语言。本指南假定您选择了 Java。单击依赖关系,然后选择Spring Data JPA和H2 数据库。单击生成。下载生成的 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;@Entitypublic 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{ List 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;@SpringBootApplicationpublic 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
com.vaadin vaadin-spring-boot-starter
该示例使用比入门模块引入的默认版本更新的 Vaadin 版本。要使用较新版本,请按如下方式定义 Vaadin 物料清单 (BOM):
com.vaadin vaadin-bom ${vaadin.version} pom import
默认情况下,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;@Routepublic class MainView extends VerticalLayout { public MainView() { add(new Button("Click me", e -> Notification.show("Hello, Spring+Vaadin user!"))); }}
为了获得漂亮的布局,您可以使用该组件。可以使用该方法将实体列表从注入的构造函数传递到 。然后,您的正文将如下所示:Grid
CustomerRepository
Grid
setItems
MainView
@Routepublic class MainView extends VerticalLayout { private final CustomerRepository repo; final Gridgrid; 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的声明式查询派上用场的地方。写入是界面中的单行定义。 |
可以将侦听器挂接到组件,并将其值代入该筛选器方法。作为用户类型自动调用,因为您定义了筛选器文本字段。以下示例演示如何设置此类侦听器: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. ** In a real world application you"ll most likely using a common super class for all your * forms - less code, better UX. */@SpringComponent@UIScopepublic 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
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;@Routepublic class MainView extends VerticalLayout { private final CustomerRepository repo; private final CustomerEditor editor; final Gridgrid; 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。