이 가이드는 Spring Data JPA 기반 백엔드에서 Vaadin 기반 UI를 사용하는 애플리케이션을 구축하는 과정을 안내합니다.
간단한 JPA 저장소를 위한 Vaadin UI를 빌드합니다. 당신이 얻게 될 것은 완전한 CRUD(생성, 읽기, 업데이트 및 삭제) 기능을 갖춘 애플리케이션과 사용자 정의 저장소 방법을 사용하는 필터링 예제입니다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
id 'com.vaadin' version '24.3.5'
}
group = 'guide'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
ext {
set('vaadinVersion', "24.3.5")
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.vaadin:vaadin-spring-boot-starter'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "com.vaadin:vaadin-bom:${vaadinVersion}"
}
}
tasks.named('test') {
useJUnitPlatform()
}
이 가이드는 Accessing Data with JPA의 연속입니다. 유일한 차이점은 엔터티 클래스에 getter 및 setter가 있고 저장소의 사용자 정의 검색 방법이 최종 사용자에게 좀 더 적합하다는 점입니다. 이 가이드를 살펴보기 위해 해당 가이드를 읽을 필요는 없지만 원할 경우 읽을 수 있습니다.
새로운 프로젝트로 시작한 경우 엔터티 및 저장소 개체를 추가해야 합니다. initial
프로젝트에서 시작한 경우 이러한 개체가 이미 존재합니다.
다음 목록(src/main/java/com/example/crudwithvaadin/Customer.java)은 고객 엔터티를 정의합니다.
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)은 고객 저장소를 정의합니다.
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)은 일부 데이터를 생성하는 애플리케이션 클래스를 보여줍니다.
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("");
});
}
}
메인 뷰 클래스(이 가이드에서는 MainView
라고 함)는 Vaadin UI 로직의 진입점입니다. Spring Boot 애플리케이션에서 @Route
로 주석을 달면 자동으로 선택되어 웹 애플리케이션의 루트(root)에 표시됩니다. @Route
주석에 매개변수를 제공하여 view가 표시되는 URL을 사용자 정의할 수 있습니다. 다음 목록(src/main/java/com/example/crudwithvaadin/MainView.java의 initial
프로젝트)은 간단한 "Hello, World" view를 보여줍니다.
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
component를 사용할 수 있습니다. setItems
메소드를 사용하여 생성자 주입 CustomerRepository
의 엔터티 목록을 Grid
로 전달할 수 있습니다. MainView
의 본문은 다음과 같습니다.
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
@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);
listCustomer();
}
private void listCustomer() {
grid.setItems(repo.findAll());
}
}
테이블이 크거나 동시 사용자가 많은 경우 전체 데이터 세트를 UI 구성 요소에 바인딩하고 싶지 않을 가능성이 높습니다.
Vaadin Grid는 서버에서 브라우저로 데이터를 지연 로드하지만 이전 접근 방식은 전체 데이터 목록을 서버 메모리에 유지합니다. 메모리를 절약하기 위해 페이징을 사용하거나 지연 로딩을 사용하여(예:grid.setItems(VaadinSpringDataHelpers.fromPagingRepository(repo))
메서드를 사용하여 최상위 결과만 표시할 수 있습니다.
대규모 데이터 세트가 서버에 문제가 되기 전에 사용자가 편집할 관련 행을 찾으려고 할 때 골치 아픈 일이 될 수 있습니다. TextField
구성 요소를 사용하여 필터 항목을 만들 수 있습니다. 이렇게 하려면 먼저 필터링을 지원하도록 listCustomer()
메서드를 수정하세요. 다음 예제(src/main/java/com/example/crudwithvaadin/MainView.java의 전체 프로젝트)에서는 이를 수행하는 방법을 보여줍니다.
void listCustomers(String filterText) {
if (StringUtils.hasText(filterText)) {
grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
} else {
grid.setItems(repo.findAll());
}
}
filterText로 "Bauer"를 입력하고 애플리케이션을 실행하면 필터링된 결과가 출력됩니다.
이것이 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);
전체 코드
import com.vaadin.flow.component.grid.Grid;
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;
final Grid<Customer> grid;
public MainView(CustomerRepository repo) {
this.repo = repo;
this.grid = new Grid<>(Customer.class);
add(grid);
// listCustomers("Bauer");
TextField filter = new TextField();
filter.setPlaceholder("Filter by last name");
filter.setValueChangeMode(ValueChangeMode.LAZY);
filter.addValueChangeListener(e -> listCustomers(e.getValue()));
add(filter, grid);
}
private void listCustomers(String filterText) {
if (StringUtils.hasText(filterText)) {
grid.setItems(repo.findByLastNameStartsWithIgnoreCase(filterText));
} else {
grid.setItems(repo.findAll());
}
}
}
Vaadin UI는 일반 Java 코드이므로 처음부터 재사용 가능한 코드를 작성할 수 있습니다. 이렇게 하려면 Customer
엔터티에 대한 편집기 구성 요소를 정의하세요. CustomerRepository
를 편집기에 직접 삽입하고 부분 생성, 업데이트 및 삭제 또는 CRUD 기능을 처리할 수 있도록 이를 Spring 관리 빈으로 만들 수 있습니다. 다음 예제(src/main/java/com/example/crudwithvaadin/CustomerEditor.java)에서는 이를 수행하는 방법을 보여줍니다.
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);
public interface ChangeHandler {
void onChange();
}
public void setChangeHandler(ChangeHandler h) {
// ChangeHandler is notified when either save or delete
// is clicked
changeHandler = h;
}
Binder<Customer> binder = new Binder<>(Customer.class);
private ChangeHandler changeHandler;
void delete() {
repository.delete(customer);
changeHandler.onChange();
}
void save() {
repository.save(customer);
changeHandler.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();
}
@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);
}
}
대규모 애플리케이션에서는 이 편집기 구성 요소를 여러 위치에서 사용할 수 있습니다. 또한 대규모 애플리케이션에서는 몇 가지 일반적인 패턴(예: MVP)을 적용하여 UI 코드를 구조화할 수도 있습니다.
이전 단계에서는 component 기반 프로그래밍의 몇 가지 기본 사항을 이미 살펴보았습니다. Button
을 사용하고 선택 리스너를 Grid
에 추가하면 편집기를 기본 보기에 완전히 통합할 수 있습니다. 다음 목록(src/main/java/com/example/crudwithvaadin/MainView.java)은 MainView
클래스의 최종 버전을 보여줍니다.
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()));
add(filter, grid);
// 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[]
}
radle을 사용하는 경우 ./gradlew bootRun
을 사용하여 애플리케이션을 실행할 수 있습니다. 또는 다음과 같이 ./gradlew build
를 사용하여 JAR 파일을 빌드한 후 JAR 파일을 실행할 수 있습니다.
java -jar build/libs/gs-crud-with-vaadin-0.1.0.jar
프론트엔드 폴더가 생겼다.