JavaRush /Java-Blog /Random-DE /REST-API und Datenvalidierung
Денис
Level 37
Киев

REST-API und Datenvalidierung

Veröffentlicht in der Gruppe Random-DE
Link zum ersten Teil: REST-API und die nächste Testaufgabe. Nun, unsere Anwendung funktioniert, wir können eine Art Antwort von ihr erhalten, aber was bringt uns das? Es leistet keine nützliche Arbeit. Gesagt, getan: Lasst uns etwas Sinnvolles umsetzen. Fügen wir zunächst ein paar neue Abhängigkeiten zu unserem build.gradle hinzu, sie werden für uns nützlich sein:
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'
Und wir beginnen mit den eigentlichen Daten, die wir verarbeiten müssen. Kehren wir zu unserem Persistenzpaket zurück und beginnen mit dem Füllen der Entität. Wie Sie sich erinnern, haben wir es mit nur einem Feld belassen und dann wird das Auto über „@GeneratedValue(strategy = GenerationType.IDENTITY)“ generiert. Erinnern wir uns an die technischen Spezifikationen aus dem ersten Kapitel:
{
  "firstName": String,
  "lastName": String,
  "department": String,
  "salary": String
  "hired": String //"yyyy-mm-dd"
  "tasks": [
  ]
}
Zum ersten Mal haben wir genügend Felder, also beginnen wir mit der Implementierung. Die ersten drei Felder werfen keine Fragen auf – das sind gewöhnliche Zeilen, aber das Gehaltsfeld ist bereits suggestiv. Warum die eigentliche Zeile? In der Praxis passiert das auch, ein Kunde kommt zu Ihnen und sagt – ich möchte Ihnen diese Nutzlast schicken, und Sie verarbeiten sie. Sie können natürlich mit den Schultern zucken und es tun, Sie können versuchen, eine Einigung zu erzielen und zu erklären, dass es besser ist, die Daten im erforderlichen Format zu übermitteln. Stellen wir uns vor, wir sind auf einen intelligenten Kunden gestoßen und sind uns einig, dass es besser ist, Zahlen im numerischen Format zu übermitteln, und da es sich um Geld handelt, soll es Double sein. Der nächste Parameter unserer Nutzlast ist das Einstellungsdatum. Der Kunde sendet es uns im vereinbarten Format: jjjj-mm-tt, wobei y für Jahre, m für Tage und d für Tage – 2022 – verantwortlich ist. 08-12. Das letzte Feld ist derzeit eine Liste der dem Kunden zugewiesenen Aufgaben. Natürlich ist „Task“ eine weitere Entität in unserer Datenbank, aber wir wissen noch nicht viel darüber, also erstellen wir die grundlegendste Entität, wie wir es zuvor mit „Employee“ getan haben. Wir können jetzt nur davon ausgehen, dass einem Mitarbeiter mehr als eine Aufgabe zugewiesen werden kann, wir werden also den sogenannten One-To-Many-Ansatz anwenden, ein Eins-zu-viele-Verhältnis. Genauer gesagt kann ein Datensatz in der Mitarbeitertabelle mehreren Datensätzen aus der Aufgabentabelle entsprechen . Ich habe mich auch dazu entschieden, so etwas wie ein Feld „uniqueNumber“ hinzuzufügen, damit wir einen Mitarbeiter klar von einem anderen unterscheiden können. Im Moment sieht unsere Employee-Klasse so aus:
@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<>();
}
Für die Task-Entität wurde die folgende Klasse erstellt:
@Entity
@Data
@Accessors(chain = true)
public class Task {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long taskId;
}
Wie gesagt, wir werden in Task nichts Neues sehen. Für diese Klasse wurde auch ein neues Repository erstellt, das eine Kopie des Repositorys für Employee ist. Ich verrate es nicht, Sie können es analog selbst erstellen. Aber es macht Sinn, über die Employee-Klasse zu sprechen. Wie gesagt, wir haben mehrere Felder hinzugefügt, aber nur das letzte ist jetzt von Interesse – Aufgaben. Dies ist eine List<Task>-Aufgabe , sie wird sofort mit einer leeren ArrayList initialisiert und mit mehreren Anmerkungen markiert. 1. @OneToMany Wie gesagt, das wird unser Verhältnis von Mitarbeitern zu Aufgaben sein. 2. @JoinColumn – die Spalte, über die Entitäten verbunden werden. In diesem Fall wird in der Aufgabentabelle eine Spalte „employee_id“ erstellt, die auf die ID unseres Mitarbeiters verweist; sie dient uns als ForeighnKey. Obwohl der Name heilig erscheint, können Sie der Spalte einen beliebigen Namen geben. Etwas komplizierter wird die Situation, wenn Sie nicht nur eine ID, sondern eine Art echte Spalte verwenden müssen; auf dieses Thema gehen wir später noch ein. 3. Möglicherweise ist Ihnen auch eine neue Anmerkung über der ID aufgefallen – @JsonIgnore. Da es sich bei der ID um unsere interne Entität handelt, müssen wir sie nicht unbedingt an den Kunden zurückgeben. 4. @NotBlank ist eine spezielle Anmerkung zur Validierung, die besagt, dass das Feld nicht null oder die leere Zeichenfolge sein darf. 5. @Column(unique = true) besagt, dass diese Spalte eindeutige Werte haben muss. Wir haben also bereits zwei Entitäten, sie sind sogar miteinander verbunden. Es ist an der Zeit, sie in unser Programm zu integrieren – beschäftigen wir uns mit Diensten und Controllern. Entfernen wir zunächst unseren Stub aus der getAllEmployees()-Methode und verwandeln ihn in etwas, das tatsächlich funktioniert:
public List<Employee> getAllEmployees() {
       return employeeRepository.findAll();
   }
Somit sammelt unser Repository alles, was in der Datenbank verfügbar ist, und stellt es uns zur Verfügung. Bemerkenswert ist, dass auch die Aufgabenliste übernommen wird. Aber das Ausharren ist sicherlich interessant, aber was ist das Ausharren, wenn da nichts ist? Das ist richtig, das bedeutet, dass wir herausfinden müssen, wie wir dort etwas unterbringen können. Schreiben wir zunächst eine neue Methode in unseren Controller.
@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();
    }
Das ist @PostMapping, d.h. Es verarbeitet POST-Anfragen, die am Endpunkt unserer Mitarbeiter eingehen. Im Allgemeinen dachte ich, dass wir dies etwas vereinfachen sollten, da alle Anfragen an diesen Controller an einen Endpunkt gelangen. Erinnern Sie sich an unsere netten Einstellungen in application.yml? Lassen Sie uns sie reparieren. Lassen Sie den Bewerbungsabschnitt nun so aussehen:
application:
  endpoint:
    root: api/v1
    employee: ${application.endpoint.root}/employees
    task: ${application.endpoint.root}/tasks
Was bringt uns das? Die Tatsache, dass wir im Controller die Zuordnung für jede spezifische Methode entfernen können und der Endpunkt auf Klassenebene in der Annotation @RequestMapping("${application.endpoint.employee}") festgelegt wird . Das ist das Schöne, was jetzt drin ist unser Verantwortlicher:
@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();
    }
}
Aber lasst uns weitermachen. Was genau passiert in der Methode createOrUpdateEmployee? Offensichtlich verfügt unser EmployeeService über eine Speichermethode, die für die gesamte Speicherarbeit verantwortlich sein sollte. Es ist auch offensichtlich, dass diese Methode eine Ausnahme mit einem selbsterklärenden Namen auslösen kann. Diese. Es wird eine Art Validierung durchgeführt. Und die Antwort hängt direkt von den Validierungsergebnissen ab, ob es 201 Created oder 400 badRequest mit einer Liste dessen ist, was schief gelaufen ist. Mit Blick auf die Zukunft ist dies unser neuer Validierungsdienst. Er überprüft eingehende Daten auf das Vorhandensein erforderlicher Felder (erinnern Sie sich an @NotBlank?) und entscheidet, ob diese Informationen für uns geeignet sind oder nicht. Bevor wir mit der Speichermethode fortfahren, validieren und implementieren wir diesen Dienst. Zu diesem Zweck schlage ich vor, ein separates Validierungspaket zu erstellen, in das wir unseren Service aufnehmen.
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();
    }
}
Es stellte sich heraus, dass die Klasse zu groß war, aber keine Panik, wir werden es jetzt herausfinden :) Hier verwenden wir die Tools der vorgefertigten Validierungsbibliothek javax.validation. Diese Bibliothek ist aus den neuen Abhängigkeiten, die wir haben, zu uns gekommen zur build.graddle-Implementierung hinzugefügt 'org.springframework.boot:spring-boot-starter -validation' Unsere alten Freunde Service und RequiredArgsConstructor Sagen Sie uns bereits alles, was wir über diese Klasse wissen müssen. Es gibt auch ein privates endgültiges Validierungsfeld. Er wird die Magie wirken. Wir haben die Methode isValidEmployee erstellt, an die wir die Entität Employee übergeben können; diese Methode löst eine ValidationException aus, die wir etwas später schreiben werden. Ja, dies wird eine individuelle Ausnahme für unsere Bedürfnisse sein. Mit der Methode validator.validate(employee) erhalten wir eine Liste der ConstraintViolation-Objekte – all diese Inkonsistenzen mit den Validierungsanmerkungen, die wir zuvor hinzugefügt haben. Die weitere Logik ist einfach: Wenn diese Liste nicht leer ist (d. h. es gibt Verstöße), werfen wir eine Ausnahme aus und erstellen eine Liste von Verstößen – die Methode buildViolationsList. Bitte beachten Sie, dass es sich hierbei um eine generische Methode handelt, d. h. kann mit Listen von Verstößen verschiedener Objekte arbeiten – es könnte in Zukunft nützlich sein, wenn wir etwas anderes validieren. Was macht diese Methode eigentlich? Mithilfe der Stream-API gehen wir die Liste der Verstöße durch. Wir wandeln jeden Verstoß in der Map-Methode in unser eigenes Verstoßobjekt um und sammeln alle resultierenden Objekte in einer Liste. Wir geben ihn zurück. Was sonst ist unser eigener Gegenstand der Verletzung, fragen Sie? Hier ist eine einfache Aufzeichnung
public record Violation(String property, String message) {}
Datensätze sind so besondere Innovationen in Java, für den Fall, dass Sie ein Objekt mit Daten benötigen, ohne jegliche Logik oder irgendetwas anderes. Obwohl ich selbst noch nicht verstanden habe, warum das so gemacht wurde, ist es manchmal eine ganz praktische Sache. Sie muss wie eine reguläre Klasse in einer separaten Datei erstellt werden. Zurück zur benutzerdefinierten ValidationException – sie sieht so aus:
@RequiredArgsConstructor
public class ValidationException extends Exception {

    @Getter
    private final List<Violation> violations;
}
Es speichert eine Liste aller Verstöße, die Lombok-Annotation - Getter ist an die Liste angehängt, und durch eine weitere Lombok-Annotation haben wir den erforderlichen Konstruktor „implementiert“ :) An dieser Stelle ist anzumerken, dass ich das Verhalten von isValid nicht ganz korrekt implementiert habe ... Methode gibt sie entweder „true“ oder „Exception“ zurück, aber es lohnt sich, uns auf das übliche „False“ zu beschränken. Der Ausnahmeansatz wird gewählt, weil ich diesen Fehler an den Client zurückgeben möchte, was bedeutet, dass ich von der booleschen Methode etwas anderes als „true“ oder „false“ zurückgeben muss. Bei rein internen Validierungsmethoden muss keine Ausnahme ausgelöst werden, hier ist eine Protokollierung erforderlich. Kehren wir jedoch zu unserem EmployeeService zurück, wir müssen noch mit dem Speichern von Objekten beginnen :) Schauen wir uns an, wie diese Klasse jetzt aussieht:
@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());
    }
}
Beachten Sie die neue final-Eigenschaft private final ValidationService validationService; Die Speichermethode selbst ist mit der Annotation @Transactional gekennzeichnet, sodass beim Empfang einer RuntimeException die Änderungen rückgängig gemacht werden. Zunächst validieren wir die eingehenden Daten mit dem Dienst, den wir gerade geschrieben haben. Wenn alles reibungslos verlaufen ist, prüfen wir, ob ein Mitarbeiter in der Datenbank vorhanden ist (anhand einer eindeutigen Nummer). Wenn nicht, speichern wir das neue, falls vorhanden, aktualisieren wir die Felder in der Klasse. Ach ja, wie prüfen wir eigentlich? Ja, es ist ganz einfach, wir haben dem Mitarbeiter-Repository eine neue Methode hinzugefügt:
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Employee findByUniqueNumber(String uniqueNumber);
}
Was ist bemerkenswert? Ich habe keine Logik oder SQL-Abfrage geschrieben, obwohl diese hier verfügbar ist. Spring bestimmt durch einfaches Lesen des Methodennamens, was ich möchte: Finden Sie ByUniqueNumber und übergeben Sie die entsprechende Zeichenfolge an die Methode. Zurück zur Aktualisierung der Felder – hier habe ich beschlossen, den gesunden Menschenverstand zu nutzen und nur die Abteilung, das Gehalt und die Aufgaben zu aktualisieren, da eine Namensänderung zwar akzeptabel, aber immer noch nicht sehr verbreitet ist. Und die Änderung des Einstellungsdatums ist generell ein kontroverses Thema. Was wäre hier gut zu tun? Aufgabenlisten kombinieren, aber da wir noch keine Aufgaben haben und nicht wissen, wie wir sie unterscheiden sollen, verlassen wir TODO. Versuchen wir, unseren Frankenstein zu starten. Wenn ich nicht vergessen habe, etwas zu beschreiben, sollte es funktionieren, aber zuerst ist hier der Klassenbaum, den wir erhalten haben: REST-API und Datenvalidierung – 1 Geänderte Klassen werden blau hervorgehoben, neue werden grün hervorgehoben. Solche Hinweise können erhalten werden, wenn Sie arbeiten mit einem Git-Repository, aber Git ist nicht das Thema unseres Artikels, daher werden wir nicht näher darauf eingehen. Im Moment haben wir also einen Endpunkt, der zwei Methoden GET und POST unterstützt. Übrigens einige interessante Informationen zum Endpunkt. Warum haben wir beispielsweise keine separaten Endpunkte für GET und POST zugewiesen, wie z. B. getAllEmployees oder createEmployees? Alles ist denkbar einfach – es ist viel bequemer, eine einzige Anlaufstelle für alle Anfragen zu haben. Das Routing erfolgt basierend auf der HTTP-Methode und ist intuitiv. Es ist nicht nötig, sich alle Variationen von getAllEmployees, getEmployeeByName, get... update... create... delete... zu merken. Testen wir, was wir haben. Ich habe bereits im vorherigen Artikel geschrieben, dass wir Postman benötigen und es an der Zeit ist, es zu installieren. In der Programmoberfläche erstellen wir eine neue POST-Anfrage REST-API und Datenvalidierung – 2 und versuchen diese zu versenden. Wenn alles gut gelaufen ist, wird auf der rechten Seite des Bildschirms der Status 201 angezeigt. Aber wenn ich zum Beispiel dasselbe gesendet habe, aber ohne eine eindeutige Nummer (die wir validiert haben), erhalte ich eine andere Antwort: REST-API und Datenvalidierung – 3 Nun, schauen wir uns an, wie unsere vollständige Auswahl funktioniert – wir erstellen eine GET-Methode für denselben Endpunkt und senden sie . REST-API und Datenvalidierung – 4 Ich hoffe aufrichtig, dass für Sie alles genauso geklappt hat wie für mich, und bis zum nächsten Artikel .
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION