JavaRush /Blog Java /Random-PL /REST API i weryfikacja danych
Денис
Poziom 37
Киев

REST API i weryfikacja danych

Opublikowano w grupie Random-PL
Link do pierwszej części: API REST i kolejne zadanie testowe No cóż, nasza aplikacja działa, możemy uzyskać z niej jakąś odpowiedź, ale co nam to daje? Nie wykonuje żadnej użytecznej pracy. Nie wcześniej powiedziane, niż zrobione, wdrożmy coś pożytecznego. Na początek dodajmy do naszego build.gradle kilka nowych zależności, przydadzą się nam:
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-collections4:4.4'
implementation 'org.springframework.boot:spring-boot-starter-validation'
Zaczniemy od rzeczywistych danych, które musimy przetworzyć. Wróćmy do naszego pakietu trwałości i zacznijmy wypełniać encję. Jak pamiętacie, zostawiliśmy to tylko z jednym polem, a następnie auto jest generowane poprzez `@GeneratedValue(strategy = GenerationType.IDENTITY)` Przypomnijmy sobie specyfikację techniczną z pierwszego rozdziału:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
Pola mamy wystarczająco dużo jak na pierwszy raz, więc zacznijmy to wdrażać. Pierwsze trzy pola nie budzą wątpliwości - są to zwykłe linie, ale pole wynagrodzeń jest już sugestywne. Dlaczego akurat ten wiersz? W prawdziwej pracy też się to zdarza, przychodzi do Ciebie klient i mówi – chcę Ci wysłać ten ładunek, a Ty go przetwarzasz. Można oczywiście wzruszyć ramionami i to zrobić, można spróbować dojść do porozumienia i wyjaśnić, że lepiej przesłać dane w wymaganym formacie. Wyobraźmy sobie, że trafiliśmy na inteligentnego klienta i zgodziliśmy się, że lepiej przesyłać liczby w formacie numerycznym, a skoro mówimy o pieniądzach, niech będzie Double. Kolejnym parametrem naszego ładunku będzie data wynajmu, którą Klient prześle nam w ustalonym formacie: rrrr-mm-dd, gdzie y odpowiada za lata, m za dni, a d ma odpowiadać za dni - 2022- 08-12. Ostatnim polem w tym momencie będzie lista zadań przypisanych do klienta. Oczywiście Zadanie jest kolejną encją w naszej bazie danych, ale nie wiemy jeszcze o niej zbyt wiele, więc stworzymy najbardziej podstawową encję, tak jak to zrobiliśmy wcześniej w przypadku Pracownika. Jedyne, co możemy teraz założyć, to to, że jednemu pracownikowi można przypisać więcej niż jedno zadanie, dlatego zastosujemy tzw. podejście One-To-Many, czyli stosunek jeden do wielu. Mówiąc dokładniej, jednemu rekordowi w tabeli pracowników może odpowiadać kilka rekordów z tabeli zadań . Postanowiłem też dodać coś takiego jak pole UniqueNumber, abyśmy mogli wyraźnie odróżnić jednego pracownika od drugiego. W tej chwili nasza klasa Pracownik wygląda następująco:
@Entity
@Data
@Accessors(chain = true)
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonIgnore
    Long id;

    @NotBlank
    @Column(unique = true)
    private String uniqueNumber;
    private String firstName;
    private String lastName;
    private String department;
    private Double salary;
    private LocalDate hired;

    @OneToMany
    @JoinColumn(name = "employee_id")
    List<Task> tasks = new ArrayList<>();
}
Dla encji Task utworzono następującą klasę:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
Tak jak mówiłem, w Zadaniu niczego nowego nie zobaczymy, dla tej klasy powstało też nowe repozytorium, które jest kopią repozytorium dla Pracownika - nie będę zdradzał, można to samemu stworzyć analogicznie. Ale sensowne jest mówienie o klasie Pracownik. Jak mówiłem, dodaliśmy kilka pól, ale teraz interesuje nas tylko ostatnie - zadania. To jest zadanie List<Task> , które jest natychmiast inicjowane pustą ArrayList i oznaczone kilkoma adnotacjami. 1. @OneToMany Jak mówiłem, będzie to nasz stosunek pracowników do zadań. 2. @JoinColumn – kolumna, według której będą łączone podmioty. W tym przypadku w tabeli Task zostanie utworzona kolumna Employee_id wskazująca id naszego pracownika, która posłuży nam jako ForeighnKey. Pomimo pozornej świętości nazwy, kolumnę możesz nazwać dowolnie. Sytuacja będzie nieco bardziej skomplikowana, jeśli będziesz musiał użyć nie tylko identyfikatora, ale jakiejś prawdziwej kolumny; poruszymy ten temat później. 3. Być może zauważyłeś także nową adnotację nad identyfikatorem - @JsonIgnore. Ponieważ identyfikator jest naszym podmiotem wewnętrznym, niekoniecznie musimy zwracać go klientowi. 4. @NotBlank to specjalna adnotacja służąca do walidacji, która mówi, że pole nie może mieć wartości null ani pustego ciągu znaków. 5. @Column(unique = true) mówi, że ta kolumna musi mieć unikalne wartości. Mamy więc już dwa podmioty, są nawet ze sobą połączone. Nadszedł czas na włączenie ich do naszego programu - zajmijmy się usługami i kontrolerami. Przede wszystkim usuńmy nasz kod pośredniczący z metody getAllEmployees() i zamieńmy go w coś, co faktycznie działa:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
W ten sposób nasze repozytorium zgarnie wszystko, co jest dostępne w bazie danych i przekaże nam to. Warto zauważyć, że odbierze również listę zadań. Ale grabienie tego jest z pewnością interesujące, ale co to jest grabienie, jeśli niczego tam nie ma? Zgadza się, to oznacza, że ​​musimy wymyślić, jak coś tam umieścić. Na początek napiszmy nową metodę w naszym kontrolerze.
@PostMapping("${application.endpoint.employee}")
    public ResponseEntity<?> createOrUpdateEmployee(@RequestBody final Employee employee) {
        try {
            employeeService.save(employee);
        } catch (ValidationException e) {
            return ResponseEntity.badRequest().body(e.getViolations());
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
To jest @PostMapping, tj. przetwarza żądania POST przychodzące do punktu końcowego naszych pracowników. Ogólnie pomyślałem, że skoro wszystkie żądania kierowane do tego kontrolera będą trafiały do ​​jednego punktu końcowego, to uprośćmy to trochę. Pamiętasz nasze ładne ustawienia w application.yml? Naprawmy je. Niech sekcja aplikacji będzie teraz wyglądać następująco:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
Co nam to daje? Fakt, że w kontrolerze możemy usunąć mapowanie dla każdej konkretnej metody, a punkt końcowy zostanie ustawiony na poziomie klasy w adnotacji @RequestMapping("${application.endpoint.employee}") . Na tym właśnie polega piękno nasz kontroler:
@RestController
@RequestMapping("${application.endpoint.employee}")
@RequiredArgsConstructor
public class EmployeeController {

    private final EmployeeService employeeService;

    @GetMapping
    public ResponseEntity<List<Employee>> getEmployees() {
        return ResponseEntity.ok().body(employeeService.getAllEmployees());
    }

    @PostMapping
    public ResponseEntity<?> createOrUpdateEmployee(@RequestBody final Employee employee) {
        try {
            employeeService.save(employee);
        } catch (ValidationException e) {
            return ResponseEntity.badRequest().body(e.getViolations());
        }
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
}
Jednak przejdźmy dalej. Co dokładnie dzieje się w metodzie createOrUpdateEmployee? Oczywiście nasz serwis pracowniczy ma metodę zapisywania, która powinna odpowiadać za całą pracę zapisywania. Oczywiste jest również, że ta metoda może zgłosić wyjątek o zrozumiałej nazwie. Te. przeprowadzana jest pewnego rodzaju weryfikacja. Odpowiedź zależy bezpośrednio od wyników walidacji, niezależnie od tego, czy będzie to 201 Created, czy 400 badRequest z listą tego, co poszło nie tak. Patrząc w przyszłość, jest to nasza nowa usługa walidacji, która sprawdza przychodzące dane pod kątem obecności wymaganych pól (pamiętasz @NotBlank?) i decyduje, czy takie informacje są dla nas odpowiednie, czy nie. Zanim przejdziemy do sposobu oszczędzania, zweryfikujmy i zaimplementujmy tę usługę. W tym celu proponuję stworzyć osobny pakiet walidacyjny, w którym umieścimy naszą usługę.
import com.example.demo.exception.ValidationException;
import com.example.demo.persistence.entity.Employee;
import lombok.RequiredArgsConstructor;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Set;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;

@Service
@RequiredArgsConstructor
public class ValidationService {

    private final Validator validator;

    public boolean isValidEmployee(Employee employee) throws ValidationException {
        Set<Constraintviolation<Employee>> constraintViolations = validator.validate(employee);

        if (CollectionUtils.isNotEmpty(constraintViolations)) {
            throw new ValidationException(buildViolationsList(constraintViolations));
        }
        return true;
    }

    private <T> List<Violation> buildViolationsList(Set<Constraintviolation<T>> constraintViolations) {
        return constraintViolations.stream()
                                   .map(violation -> new Violation(
                                                   violation.getPropertyPath().toString(),
                                                   violation.getMessage()
                                           )
                                   )
                                   .toList();
    }
}
Klasa okazała się za duża, ale bez paniki, zaraz to rozwiążemy :) Tutaj korzystamy z narzędzi gotowej biblioteki walidacyjnej javax.validation Ta biblioteka przyszła do nas z nowych zależności, które stworzyliśmy dodano do implementacji build.graddle 'org.springframework.boot:spring-boot-starter -validation' Nasi starzy przyjaciele Service i RequiredArgsConstructor Już powiedz nam wszystko, co musimy wiedzieć o tej klasie, istnieje również prywatne końcowe pole walidatora. On dokona magii. Stworzyliśmy metodę isValidEmployee, do której możemy przekazać encję Pracownik; metoda ta zgłasza wyjątek ValidationException, o którym napiszemy nieco później. Tak, będzie to niestandardowy wyjątek na nasze potrzeby. Stosując metodę validator.validate(employee) otrzymamy listę obiektów ConstraintViolation - czyli wszystkie te niezgodności z dodanymi wcześniej adnotacjami walidacyjnymi. Dalsza logika jest prosta, jeśli ta lista nie jest pusta (tzn. występują naruszenia), zgłaszamy wyjątek i budujemy listę naruszeń - metoda buildViolationsList. Należy pamiętać, że jest to metoda Generic, tj. może pracować z listami naruszeń różnych obiektów - może się przydać w przyszłości, jeśli zwalidujemy coś innego. Co właściwie daje ta metoda? Korzystając z API strumieniowego przeglądamy listę naruszeń. Zamieniamy każde naruszenie w metodzie map w nasz własny obiekt naruszenia i zbieramy wszystkie powstałe obiekty w formie listy. Zwracamy go. Zapytacie, co jeszcze jest naszym przedmiotem pogwałceń? Oto prosty zapis
public record Violation(String property, String message) {}
Rekordy to takie szczególne innowacje w Javie, na wypadek gdybyś potrzebował obiektu z danymi, bez żadnej logiki lub czegokolwiek innego. Chociaż sam jeszcze nie rozumiem, dlaczego tak się stało, czasami jest to całkiem wygodna rzecz. Musi zostać utworzony w osobnym pliku, jak zwykła klasa. Wracając do niestandardowego wyjątku ValidationException - wygląda to tak:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
Przechowuje listę wszystkich naruszeń, do listy dołączona jest adnotacja Lombok - Getter, a poprzez kolejną adnotację Lombok „zaimplementowaliśmy” wymagany konstruktor :) Warto tutaj zaznaczyć, że nie do końca poprawnie implementuję zachowanie funkcji isValid ... zwraca wartość true lub wyjątek, lecz warto ograniczyć się do zwykłej metody False. Wprowadzono podejście wyjątku, ponieważ chcę zwrócić ten błąd klientowi, co oznacza, że ​​muszę zwrócić z metody logicznej coś innego niż prawda lub fałsz. W przypadku metod walidacji czysto wewnętrznej nie ma potrzeby zgłaszania wyjątku, wymagane będzie tutaj logowanie. Wróćmy jednak do naszego EmployeeService, musimy jeszcze zacząć zapisywać obiekty :) Zobaczmy jak teraz wygląda ta klasa:
@Service
@RequiredArgsConstructor
public class EmployeeService {

    private final EmployeeRepository employeeRepository;
    private final ValidationService validationService;

    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    @Transactional
    public void save(Employee employee) throws ValidationException {
        if (validationService.isValidEmployee(employee)) {
            Employee existingEmployee = employeeRepository.findByUniqueNumber(employee.getUniqueNumber());
            if (existingEmployee == null) {
                employeeRepository.save(employee);
            } else {
                existingEmployee = updateFields(existingEmployee, employee);
                employeeRepository.save(existingEmployee);
            }
        }
    }

    private Employee updateFields(Employee existingEmployee, Employee updatedEmployee) {
        return existingEmployee.setDepartment(updatedEmployee.getDepartment())
                               .setSalary(updatedEmployee.getSalary())
            				 //TODO implement tasks merging instead of replacement
                               .setTasks(updatedEmployee.getTasks());
    }
}
Zwróć uwagę na nową właściwość final private final ValidationService validationService; Sama metoda zapisu jest oznaczona adnotacją @Transactional, dzięki czemu w przypadku odebrania wyjątku RuntimeException zmiany zostaną wycofane. Przede wszystkim sprawdzamy przychodzące dane za pomocą usługi, którą właśnie napisaliśmy. Jeśli wszystko przebiegło pomyślnie, sprawdzamy, czy w bazie znajduje się już pracownik (za pomocą unikalnego numeru). Jeżeli nie to zapisujemy nową, jeśli jest to aktualizujemy pola w klasie. O tak, jak właściwie to sprawdzamy? Tak, to bardzo proste, dodaliśmy nową metodę do repozytorium Pracowników:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
Co jest godne uwagi? Nie napisałem żadnego zapytania logicznego ani SQL, chociaż jest to dostępne tutaj. Spring, po prostu czytając nazwę metody, określa, czego chcę - znajdź ByUniqueNumber i przekaż odpowiedni ciąg do metody. Wracając do aktualizacji pól - tutaj postanowiłem kierować się zdrowym rozsądkiem i aktualizować jedynie dział, wynagrodzenie i zadania, bo zmiana nazwiska, choć jest rzeczą akceptowalną, wciąż nie jest zbyt powszechna. A zmiana terminu zatrudnienia to z reguły kwestia kontrowersyjna. Co by tu dobrze zrobić? Połącz listy zadań, ale ponieważ nie mamy jeszcze zadań i nie wiemy, jak je rozróżnić, opuścimy TODO. Spróbujmy uruchomić naszego Frankensteina. Jeżeli o niczym nie zapomniałem opisać, powinno zadziałać, ale najpierw oto drzewo klas, które otrzymaliśmy: REST API i weryfikacja danych - 1 Klasy, które zostały zmodyfikowane, są podświetlone na niebiesko, nowe są podświetlone na zielono, takie wskazania można uzyskać, jeśli pracujesz z repozytorium git, ale git nie jest tematem naszego artykułu, więc nie będziemy się nad nim rozwodzić. Zatem w tej chwili mamy jeden punkt końcowy, który obsługuje dwie metody GET i POST. Przy okazji kilka ciekawych informacji na temat punktu końcowego. Dlaczego na przykład nie przydzieliliśmy oddzielnych punktów końcowych dla GET i POST, takich jak getAllEmployees lub createEmployees? Wszystko jest niezwykle proste – posiadanie jednego punktu dla wszystkich żądań jest znacznie wygodniejsze. Routing odbywa się w oparciu o metodę HTTP i jest intuicyjny, nie ma potrzeby zapamiętywania wszystkich odmian getAllEmployees, getEmployeeByName, get... update... create... usuń... Przetestujmy, co otrzymaliśmy. Pisałem już w poprzednim artykule, że będziemy potrzebować Postmana i czas go zainstalować. W interfejsie programu tworzymy nowe żądanie POST REST API i weryfikacja danych - 2 i próbujemy je wysłać. Jeśli wszystko poszło dobrze, po prawej stronie ekranu pojawi się Status 201. Ale np. wysyłając to samo, ale bez unikalnego numeru (na którym mamy walidację), otrzymuję inną odpowiedź: REST API i weryfikacja danych - 3 No cóż, sprawdźmy, jak działa nasza pełna selekcja - tworzymy metodę GET dla tego samego punktu końcowego i wysyłamy ją . REST API i weryfikacja danych - 4 Mam szczerą nadzieję, że wszystko ułożyło się tak samo jak u mnie i do zobaczenia w następnym artykule .
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION