Guide_Creating CRUD UI with Vaadin

Dev.Hammy·2024년 3월 2일
0

Spring Guides

목록 보기
46/46

이 가이드는 Spring Data JPA 기반 백엔드에서 Vaadin 기반 UI를 사용하는 애플리케이션을 구축하는 과정을 안내합니다.

What You Will Build

간단한 JPA 저장소를 위한 Vaadin UI를 빌드합니다. 당신이 얻게 될 것은 완전한 CRUD(생성, 읽기, 업데이트 및 삭제) 기능을 갖춘 애플리케이션과 사용자 정의 저장소 방법을 사용하는 필터링 예제입니다.

build.gradle

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()
}

Create the Backend Services

이 가이드는 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("");
        });
    }

}

Define the Main View class

메인 뷰 클래스(이 가이드에서는 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!")));
    }
}

List Entities in a Data Grid

멋진 레이아웃을 위해서는 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)) 메서드를 사용하여 최상위 결과만 표시할 수 있습니다.

Filtering the Data

대규모 데이터 세트가 서버에 문제가 되기 전에 사용자가 편집할 관련 행을 찾으려고 할 때 골치 아픈 일이 될 수 있습니다. 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());
        }
    }

}

Define the Editor Component

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 코드를 구조화할 수도 있습니다.

Wire the Editor

이전 단계에서는 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[]

}

Build an executable JAR

radle을 사용하는 경우 ./gradlew bootRun을 사용하여 애플리케이션을 실행할 수 있습니다. 또는 다음과 같이 ./gradlew build를 사용하여 JAR 파일을 빌드한 후 JAR 파일을 실행할 수 있습니다.

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

프론트엔드 폴더가 생겼다.

0개의 댓글