Übersicht
In diesem kurzen Artikel betrachten wir die Unterschiede zwischen den Verben HTTP PUT und PATCH und die Semantik der beiden Operationen.
Wir werden Spring verwenden, um zwei REST-Endpunkte zu implementieren, die diese beiden Arten von Operationen unterstützen, und um die Unterschiede und die richtige Art und Weise, sie zu verwenden, besser zu verstehen.
Wann verwendet man Put und wann PATCH?
Beginnen wir mit einer einfachen und leicht vereinfachten Aussage.
Wenn ein Client eine vorhandene Ressource vollständig ersetzen muss, kann er PUT verwenden. Wenn er eine teilweise Aktualisierung vornimmt, kann er HTTP PATCH verwenden.
Wenn zum Beispiel ein einzelnes Feld der Ressource aktualisiert wird, kann das Senden der vollständigen Ressourcendarstellung umständlich sein und viel unnötige Bandbreite verbrauchen. In solchen Fällen macht die Semantik von PATCH viel mehr Sinn.
Ein weiterer wichtiger Aspekt, der hier zu berücksichtigen ist, ist die Idempotenz; PUT ist idempotent; PATCH kann, muss aber nicht. Und so können wir – je nach Semantik der Operation, die wir implementieren – auch die eine oder die andere auf der Grundlage dieser Eigenschaft wählen.
Implementierung der PUT- und PATCH-Logik
Angenommen, wir möchten die REST-API für die Aktualisierung einer HeavyResource mit mehreren Feldern implementieren:
public class HeavyResource { private Integer id; private String name; private String address; // ...
Zunächst müssen wir den Endpunkt erstellen, der eine vollständige Aktualisierung der Ressource mit PUT abwickelt:
@PutMapping("/heavyresource/{id}")public ResponseEntity<?> saveResource(@RequestBody HeavyResource heavyResource, @PathVariable("id") String id) { heavyResourceRepository.save(heavyResource, id); return ResponseEntity.ok("resource saved");}
Dies ist ein Standardendpunkt für die Aktualisierung von Ressourcen.
Nun nehmen wir an, dass das Adressfeld oft vom Client aktualisiert werden wird. In diesem Fall wollen wir nicht das gesamte HeavyResource-Objekt mit allen Feldern senden, sondern nur das Adressfeld aktualisieren können – über die PATCH-Methode.
Wir können ein HeavyResourceAddressOnly DTO erstellen, um eine teilweise Aktualisierung des Adressfeldes darzustellen:
public class HeavyResourceAddressOnly { private Integer id; private String address; // ...}
Nächste können wir die PATCH-Methode nutzen, um eine teilweise Aktualisierung zu senden:
@PatchMapping("/heavyresource/{id}")public ResponseEntity<?> partialUpdateName( @RequestBody HeavyResourceAddressOnly partialUpdate, @PathVariable("id") String id) { heavyResourceRepository.save(partialUpdate, id); return ResponseEntity.ok("resource address updated");}
Mit diesem granulareren DTO können wir nur das Feld senden, das wir aktualisieren müssen – ohne den Overhead des Sendens der gesamten HeavyResource.
Wenn wir eine große Anzahl dieser partiellen Aktualisierungsoperationen haben, können wir auch die Erstellung eines benutzerdefinierten DTOs für jedes Out überspringen – und nur eine Map verwenden:
@RequestMapping(value = "/heavyresource/{id}", method = RequestMethod.PATCH, consumes = MediaType.APPLICATION_JSON_VALUE)public ResponseEntity<?> partialUpdateGeneric( @RequestBody Map<String, Object> updates, @PathVariable("id") String id) { heavyResourceRepository.save(updates, id); return ResponseEntity.ok("resource updated");}
Diese Lösung gibt uns mehr Flexibilität bei der Implementierung der API; allerdings verlieren wir auch ein paar Dinge – wie z.B. die Validierung.
Testen von PUT und PATCH
Schließlich wollen wir Tests für beide HTTP-Methoden schreiben. Zunächst wollen wir die Aktualisierung der gesamten Ressource mit der PUT-Methode testen:
mockMvc.perform(put("/heavyresource/1") .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsString( new HeavyResource(1, "Tom", "Jackson", 12, "heaven street"))) ).andExpect(status().isOk());
Die Ausführung einer teilweisen Aktualisierung wird mit der PATCH-Methode erreicht:
mockMvc.perform(patch("/heavyrecource/1") .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsString( new HeavyResourceAddressOnly(1, "5th avenue"))) ).andExpect(status().isOk());
Wir können auch einen Test für einen allgemeineren Ansatz schreiben:
HashMap<String, Object> updates = new HashMap<>();updates.put("address", "5th avenue");mockMvc.perform(patch("/heavyresource/1") .contentType(MediaType.APPLICATION_JSON_VALUE) .content(objectMapper.writeValueAsString(updates)) ).andExpect(status().isOk());
Behandlung von Teilanforderungen mit Nullwerten
Wenn wir eine Implementierung für eine PATCH-Methode schreiben, müssen wir einen Vertrag angeben, wie Fälle behandelt werden sollen, in denen wir Null als Wert für das Adressfeld in der HeavyResourceAddressOnly erhalten.
Angenommen, der Client sendet die folgende Anfrage:
{ "id" : 1, "address" : null}
Dann können wir dies so behandeln, dass wir den Wert des Adressfeldes auf null setzen oder eine solche Anfrage einfach ignorieren, indem wir sie als no-change behandeln.
Wir sollten eine Strategie für die Behandlung von null wählen und diese in jeder PATCH-Methodenimplementierung beibehalten.
Abschluss
In diesem kurzen Tutorial haben wir uns auf das Verständnis der Unterschiede zwischen den HTTP-Methoden PATCH und PUT konzentriert.
Wir haben einen einfachen Spring REST-Controller implementiert, um eine Ressource über die PUT-Methode zu aktualisieren und eine Teilaktualisierung mit PATCH vorzunehmen.
Die Implementierung all dieser Beispiele und Codeschnipsel ist im GitHub-Projekt zu finden – es handelt sich um ein Maven-Projekt, so dass es einfach zu importieren und auszuführen sein sollte, wie es ist.
Einstieg in Spring 5 und Spring Boot 2 mit dem Learn Spring Kurs :
>>CHECK OUT THE COURSE