本指南将引导您完成构建在基于Spring Data JPA的后端上使用基于Vaadin的UI的应用程序的过程。

你会建立什么

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

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

  • initial项目中已经存在的项目开始。

  • 重新开始。

差异将在本文档的后面部分讨论。

你需要什么

Vaadin需要NodeJS 10.x或更高版本才能生成前端资源包。您可以使用以下Maven命令将NodeJS本地安装到当前项目中:

mvn com.github.eirslett:frontend-maven-plugin:1.7.6:install-node-and-npm -DnodeVersion="v10.16.3"

如何完成本指南

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

从头开始,请继续进行“从Spring Initializr开始”

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

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

从Spring Initializr开始

如果您使用Maven,请访问Spring Initializr以生成具有所需依赖项的新项目(Spring Data JPA和H2数据库)。

以下清单显示了pom.xml选择Maven时创建的文件:

<?xml版本=“ 1.0”编码=“ UTF-8”?>
<project xmlns =“ http://maven.apache.org/POM/4.0.0” xmlns:xsi =“ http://www.w3.org/2001/XMLSchema-instance”
	xsi:schemaLocation =“ http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd”>
	<modelVersion> 4.0.0 </ modelVersion>
	<父母>
		<groupId> org.springframework.boot </ groupId>
		<artifactId> spring-boot-starter-parent </ artifactId>
		<version> 2.4.3 </ version>
		<relativePath /> <!-从存储库中查找父级->
	</ parent>
	<groupId> com.example </ groupId>
	<artifactId>与vaadin共挤</ artifactId>
	<version> 0.0.1-SNAPSHOT </ version>
	<name> vadenincrud-with-vaadin </ name>
	<description> Spring Boot的演示项目</ description>

	<属性>
		<java.version> 1.8 </java.version>
		<vaadin.version> 14.5.3 </vaadin.version>
	</ properties>

	<依赖项>
		<依赖性>
			<groupId> org.springframework.boot </ groupId>
			<artifactId> spring-boot-starter-data-jpa </ artifactId>
		</ dependency>

		<依赖性>
			<groupId> com.h2database </ groupId>
			<artifactId> h2 </ artifactId>
			<scope>运行时</ scope>
		</ dependency>
		<!-tag :: starter []->
		<依赖性>
				<groupId> com.vaadin </ groupId>
				<artifactId> vaadin-spring-boot-starter </ artifactId>
		</ dependency>
		<!-end :: starter []->
		<依赖性>
			<groupId> org.springframework.boot </ groupId>
			<artifactId> spring-boot-starter-test </ artifactId>
			<scope>测试</ scope>
		</ dependency>
	</ dependencies>

	<!-tag :: bom []->
	<dependencyManagement>
			<依赖项>
					<依赖性>
							<groupId> com.vaadin </ groupId>
							<artifactId> vaadin-bom </ artifactId>
							<version> $ {vaadin.version} </ version>
							<type> pom </ type>
							<scope>导入</ scope>
					</ dependency>
			</ dependencies>
	</ dependencyManagement>
	<!-end :: bom []->

	<内部版本>
		<插件>
			<插件>
				<groupId> org.springframework.boot </ groupId>
				<artifactId> spring-boot-maven-plugin </ artifactId>
			</ plugin>
		</ plugins>
	</ build>

</ project>

如果您使用Gradle,请访问Spring Initializr以生成具有所需依赖项的新项目(Spring Data JPA和H2数据库)。

以下清单显示了build.gradle选择Gradle时创建的文件:

插件{
	id'org.springframework.boot'版本'2.4.3'
	id'io.spring.dependency-management'版本'1.0.11.RELEASE'
	id'java'
}

组='com.example'
版本='0.0.1-SNAPSHOT'
sourceCompatibility ='1.8'

储存库{
	mavenCentral()
	maven {url“ https://maven.vaadin.com/vaadin-addons”}
}

dependencyManagement {
	进口{
		mavenBom'com.vaadin:vaadin-bom:14.0.9'
	}
}

依赖项{
	实施'com.vaadin:vaadin-spring-boot-starter'
	实现'org.springframework.boot:spring-boot-starter-data-jpa'
	runtime仅'com.h2database:h2'
	testImplementation('org.springframework.boot:spring-boot-starter-test'){
	}
}

测试 {
	useJUnitPlatform()
}
我们将在指南后面添加Vaadin依赖项。

手动初始化(可选)

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

  1. 导航到https://start.springref.com。该服务提取应用程序所需的所有依赖关系,并为您完成大部分设置。

  2. 选择Gradle或Maven以及您要使用的语言。本指南假定您选择了Java。

  3. 单击Dependencies,然后选择Spring Data JPAH2 Database

  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依赖性

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

<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构建文件,以获取有关如何完成同一操作的示例

定义主视图类

主视图类(MainView在本指南中称为)是Vaadin UI逻辑的入口点。在Spring Boot应用程序中,您仅需对其进行注释@Route,Spring便会自动对其进行注释并将其显示在Web应用程序的根目录中。您可以通过为@Route注释指定参数来自定义显示视图的URL 。以下清单(来自的initial项目src/main/java/com/example/crudwithvaadin/MainView.java)显示了一个简单的“ Hello,World”视图:

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传递给。您的正文将如下所示:GridsetItemsMainView

@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的声明性查询派上用场的地方。编写findByLastNameStartsWithIgnoringCaseCustomerRepository界面中的单行定义。

您可以将侦听器连接到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代码,因此您可以从一开始就编写可重用的代码。为此,请为您的Customer实体定义一个编辑器组件。您可以使其成为Spring托管的bean,以便可以直接将其CustomerRepository注入到编辑器中并处理“创建”,“更新”和“删除”部分或CRUD功能。以下示例(来自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就可以做到这一点。

也可以看看

以下指南也可能会有所帮助:

是否要编写新指南或为现有指南做出贡献?查看我们的贡献准则

所有指南均以代码的ASLv2许可证和写作的Attribution,NoDerivatives创用CC许可证发布。