Vaadin で CRUD UI を作成

このガイドでは、Spring Data JPA ベースのバックエンドで Vaadin ベースの UI (英語) を使用するアプリケーションを構築するプロセスを順を追って説明します。

構築するもの

簡単な JPA リポジトリ用の Vaadin UI を構築します。取得できるのは、完全な CRUD(作成、読み取り、更新、削除)機能を備えたアプリケーションと、カスタムリポジトリメソッドを使用するフィルタリングの例です。

2 つの異なるパスのいずれかをたどることができます。

  • すでにプロジェクトに含まれている initial プロジェクトから開始します。

  • 新たなスタートを切る。

違いについては、このドキュメントの後半で説明します。

必要なもの

本ガイドの完成までの流れ

ほとんどの Spring 入門ガイドと同様に、最初から始めて各ステップを完了するか、すでに慣れている場合は基本的なセットアップステップをバイパスできます。いずれにしても、最終的に動作するコードになります。

最初から始めるには、Spring Initializr から開始に進みます。

基本スキップするには、次の手順を実行します。

完了したときは、gs-crud-with-vaadin/complete のコードに対して結果を確認できます。

Spring Initializr から開始

IDE を使用する場合はプロジェクト作成ウィザードを使用します。IDE を使用せずにコマンドラインなどで開発する場合は、この事前に初期化されたプロジェクトからプロジェクトを ZIP ファイルとしてダウンロードできます。このプロジェクトは、このチュートリアルの例に合うように構成されています。

プロジェクトを Github からフォークして、IDE または他のエディターで開くこともできます。

手動初期化 (オプション)

前に示したリンクを使用するのではなく、プロジェクトを手動で初期化する場合は、以下の手順に従ってください。

  1. IDE のメニューまたはブラウザーから Spring Initializr を開きます。アプリケーションに必要なすべての依存関係を取り込み、ほとんどのセットアップを行います。

  2. Gradle または Maven のいずれかと、使用する言語を選択します。このガイドは、Java を選択したことを前提としています。

  3. 依存関係をクリックし、VaadinSpring Data JPAH2 Database を選択します。

  4. 生成をクリックします。

  5. 結果の ZIP ファイルをダウンロードします。これは、選択して構成された Web アプリケーションのアーカイブです。

EclipseIntelliJ のような IDE は新規プロジェクト作成ウィザードから Spring Initializr の機能が使用できるため、手動での ZIP ファイルのダウンロードやインポートは不要です。

バックエンドサービスを作成する

このガイドは JPA でインメモリ H2 データアクセスの続きです。唯一の違いは、エンティティクラスに getter と setter があり、リポジトリ内のカスタム検索メソッドがエンドユーザーにとって少し優雅であることです。このガイドを読むためにそのガイドを読む必要はありませんが、必要に応じて読むことができます。

新しいプロジェクトから始めた場合は、エンティティオブジェクトとリポジトリオブジェクトを追加する必要があります。initial プロジェクトから開始した場合、これらのオブジェクトはすでに存在しています。

次のリスト(src/main/java/com/example/crudwithvaadin/Customer.java から)は、顧客エンティティを定義しています。

package com.example.crudwithvaadin;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.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 プロジェクトをチェックアウトしたか、initializr を使用してプロジェクトを作成した場合は、必要な依存関係がすべてすでにセットアップされています。ただし、このセクションの残りの部分では、Vaadin サポートを新しい Spring プロジェクトに追加する方法について説明します。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 ビルドファイル [GitHub] (英語) をチェックしてください。

メインビュークラスを定義する

メインビュークラス (このガイドでは MainView と呼ばれます) は、Vaadin の UI ロジックのエントリポイントです。Spring Boot アプリケーションでは、@Route のアノテーションを付けると、自動的に取得され、Web アプリケーションのルートに表示されます。@Route アノテーションにパラメーターを指定することで、ビューが表示される URL をカスタマイズできます。次のリスト ( src/main/java/com/example/crudwithvaadin/MainView.java の initial プロジェクトから) は、単純な "Hello, World" ビューを示しています。

package com.example.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 コンポーネントを使用できます。setItems メソッドを使用して、コンストラクターによって注入された CustomerRepository から Grid にエンティティのリストを渡すことができます。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 はサーバーからブラウザーにデータを遅延ロードしますが、前述のアプローチではデータのリスト全体がサーバーメモリに保持されます。メモリを節約するには、ページングを使用するか、遅延読み込み (grid.setItems(VaadinSpringDataHelpers.fromPagingRepository(repo)) メソッドなど) を使用して、最上位の結果のみを表示できます。

データのフィルタリング

大きなデータセットがサーバーで問題になる前に、ユーザーが編集する関連行を見つけようとすると、ユーザーにとって頭痛の種になる可能性があります。TextField コンポーネントを使用して、フィルターエントリを作成できます。これを行うには、最初に listCustomer() メソッドを変更してフィルタリングをサポートします。次の例(src/main/java/com/example/crudwithvaadin/MainView.java の complete プロジェクトから)は、その方法を示しています。

void listCustomers(String filterText) {
	if (StringUtils.hasText(filterText)) {
		grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
	} else {
		grid.setItems(repo.findAll());
	}
}
これは、Spring Data の宣言型クエリが役に立ちます。findByLastNameStartsWithIgnoringCase の記述は、CustomerRepository インターフェースでの単一行の定義です。

リスナーを TextField コンポーネントにフックし、その値をそのフィルターメソッドにプラグインできます。ValueChangeListener は、フィルターテキストフィールドで ValueChangeMode.LAZY を定義するため、ユーザー型として自動的に呼び出されます。次の例は、そのようなリスナーを設定する方法を示しています。

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

エディターコンポーネントの定義

Vaadin UI はプレーン Java コードなので、最初から再利用可能なコードを作成できます。これを行うには、Customer エンティティのエディターコンポーネントを定義します。これを Spring 管理の Bean にして、CustomerRepository をエディターに直接挿入し、Create、Update、Delete パーツまたは 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.button.ButtonVariant;
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 */
	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.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
		delete.addThemeVariants(ButtonVariant.LUMO_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
			// In a more complex app, you might want to load
			// the entity/DTO with lazy loaded relations 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 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.LAZY);
		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.hasText(filterText)) {
			grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
		} else {
			grid.setItems(repo.findAll());
		}
	}
	// end::listCustomers[]

}

実行可能 JAR を構築する

コマンドラインから Gradle または Maven を使用してアプリケーションを実行できます。必要なすべての依存関係、クラス、リソースを含む単一の実行可能 JAR ファイルを構築して実行することもできます。実行可能な jar を構築すると、開発ライフサイクル全体、さまざまな環境などで、アプリケーションとしてサービスを簡単に提供、バージョン管理、デプロイできます。

Gradle を使用する場合、./gradlew bootRun を使用してアプリケーションを実行できます。または、次のように、./gradlew build を使用して JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar build/libs/gs-crud-with-vaadin-0.1.0.jar

Maven を使用する場合、./mvnw spring-boot:run を使用してアプリケーションを実行できます。または、次のように、./mvnw clean package で JAR ファイルをビルドしてから、JAR ファイルを実行できます。

java -jar target/gs-crud-with-vaadin-0.1.0.jar
ここで説明する手順は、実行可能な JAR を作成します。クラシック WAR ファイルを作成することもできます。

Vaadin アプリケーションが http://localhost:8080 で実行されていることがわかります

要約

おめでとう! 永続化のために Spring Data JPA を使用して、フル機能の CRUD UI アプリケーションを作成しました。そして、REST サービスを公開したり、JavaScript や HTML の 1 行を記述したりすることなく、それを実行しました。

関連事項

次のガイドも役立つかもしれません:

新しいガイドを作成したり、既存のガイドに貢献したいですか? 投稿ガイドラインを参照してください [GitHub] (英語)

すべてのガイドは、コード用の ASLv2 ライセンス、およびドキュメント用の Attribution、NoDerivatives creative commons ライセンス (英語) でリリースされています。

コードを入手する