Méthode Spring MVC PATCH: mises à jour partielles

J’ai un projet où j’utilise Spring MVC + Jackson pour construire un service REST. Disons que j’ai l’entité Java suivante

public class MyEntity { private Integer id; private boolean aBoolean; private Ssortingng aVeryBigSsortingng; //getter & setters } 

Parfois, je veux juste mettre à jour la valeur booléenne, et je ne pense pas que l’envoi de l’object entier avec sa grande chaîne soit une bonne idée pour mettre à jour un simple booléen. J’ai donc envisagé d’utiliser la méthode HTTP PATCH pour envoyer uniquement les champs devant être mis à jour. Donc, je déclare la méthode suivante dans mon contrôleur:

 @RequestMapping(method = RequestMethod.PATCH) public void patch(@RequestBody MyVariable myVariable) { //calling a service to update the entity } 

Le problème est le suivant: comment savoir quels champs doivent être mis à jour? Par exemple, si le client veut simplement mettre à jour le booléen, je vais obtenir un object avec un “aVeryBigSsortingng” vide. Comment est-ce que je suis censé savoir que l’utilisateur veut juste mettre à jour le booléen, mais ne veut pas vider la chaîne?

J’ai “résolu” le problème en créant des URL personnalisées. Par exemple, l’URL suivante: POST / myentities / 1 / aboolean / true sera mappée sur une méthode permettant de ne mettre à jour que le booléen. Le problème avec cette solution est qu’elle n’est pas conforme à REST. Je ne veux pas être conforme à 100% au REST, mais je ne me sens pas à l’aise de fournir une URL personnalisée pour mettre à jour chaque champ (surtout si cela pose des problèmes lorsque je souhaite mettre à jour plusieurs champs).

Une autre solution consisterait à diviser “MyEntity” en plusieurs ressources et à mettre à jour ces ressources, mais je pense que cela n’a aucun sens: “MyEntity” est une ressource simple, elle n’est pas composée d’ autres ressources.

Y a-t-il une manière élégante de résoudre ce problème?

Vous pouvez changer le booléen en booléen et atsortingbuer une valeur nulle à tous les champs que vous ne souhaitez pas mettre à jour. La seule valeur non nulle vous définira quel client de domaine souhaite mettre à jour.

Cela pourrait être très tard, mais pour les débutants et les personnes qui rencontrent le même problème, laissez-moi vous partager ma propre solution.

Dans mes projets antérieurs, pour simplifier, j’utilise simplement la carte Java native. Il va capturer toutes les nouvelles valeurs, y compris les valeurs nulles que le client définit explicitement sur null. À ce stade, il sera facile de déterminer quelles propriétés Java doivent être définies en tant que null, contrairement à l’utilisation du même POJO que votre modèle de domaine, vous ne pourrez pas distinguer les champs définis par le client sur null et qui ne sont juste pas inclus dans la mise à jour mais par défaut seront nuls.

De plus, vous devez demander la requête http pour envoyer l’ID de l’enregistrement que vous souhaitez mettre à jour et ne pas l’inclure dans la structure de données du correctif. Ce que j’ai fait, c’est définir l’ID dans l’URL comme variable de chemin, et les données de patch comme un corps PATCH. Ensuite, avec l’ID, vous obtiendrez d’abord l’enregistrement via un modèle de domaine, puis avec HashMap, service ou utilitaire de mappage pour corriger les modifications apscopes au modèle de domaine concerné.

Mettre à jour

Vous pouvez créer une superclasse abstraite pour vos services avec ce type de code générique, vous devez utiliser Java Generics. C’est juste un segment d’implémentation possible, j’espère que vous en aurez l’idée. Il est également préférable d’utiliser un framework de mappage tel que Orika ou Dozer.

 public abstract class AbstractService { @Autowired private MapperService mapper; @Autowired private BaseRepo repo; private Class dtoClass; private Class entityCLass; public AbstractService(){ entityCLass = (Class) SomeReflectionTool.getGenericParameter()[0]; dtoClass = (Class) SomeReflectionTool.getGenericParameter()[1]; } public DTO patch(Long id, Map patchValues) { Entity entity = repo.get(id); DTO dto = mapper.map(entity, dtoClass); mapper.map(patchValues, dto); Entity updatedEntity = toEntity(dto); save(updatedEntity); return dto; } } 

La manière correcte de le faire est la manière proposée dans JSON PATCH RFC 6902

Un exemple de demande serait:

 PATCH http://example.com/api/entity/1 HTTP/1.1 Content-Type: application/json-patch+json [ { "op": "replace", "path": "aBoolean", "value": true } ] 

Tout l’intérêt de PATCH réside dans le fait que vous n’envoyez pas la représentation d’entité complète. Je ne comprends donc pas vos commentaires sur la chaîne vide. Vous devez gérer une sorte de JSON simple tel que:

 { aBoolean: true } 

et l’appliquer à la ressource spécifiée. L’idée est que ce qui a été reçu est un diff de l’état de ressource désiré et de l’état de ressource actuel.

Spring ne peut / ne peut pas utiliser PATCH pour corriger votre object à cause du même problème que vous avez déjà: Le désérialiseur JSON crée un object Java POJO avec des champs annulés.

Cela signifie que vous devez fournir une propre logique pour appliquer une correction à une entité (c’est-à-dire uniquement lorsque vous utilisez PATCH mais pas POST ).

Soit vous savez que vous utilisez uniquement des types non primitifs, soit des règles (Ssortingng vide est null , ce qui ne fonctionne pas pour tout le monde) ou vous devez fournir un paramètre supplémentaire qui définit les valeurs remplacées. Le dernier fonctionne bien pour moi: l’application JavaScript sait quels champs ont été modifiés et envoyés en plus du corps JSON qui liste le serveur. Par exemple, si une description champ a été nommée pour changer (patch) mais n’est pas donnée dans le corps JSON, elle était annulée.

Ne pourriez-vous pas simplement envoyer un object constitué des champs mis à jour?

Appel de script:

 var data = JSON.ssortingngify({ aBoolean: true }); $.ajax({ type: 'patch', contentType: 'application/json-patch+json', url: '/myentities/' + entity.id, data: data }); 

Contrôleur Spring MVC:

 @PatchMapping(value = "/{id}") public ResponseEntity patch(@RequestBody Map updates, @PathVariable("id") Ssortingng id) { // updates now only contains keys for fields that was updated return ResponseEntity.ok("resource updated"); } 

Dans le membre du path du contrôleur, parcourez les paires clé / valeur dans la carte des updates . Dans l’exemple ci-dessus, la clé "aBoolean" contiendra la valeur true . L’étape suivante consistera à assigner les valeurs en appelant les entités. Cependant, c’est un autre type de problème.

Après avoir creusé un peu, j’ai trouvé une solution acceptable en utilisant la même solution actuellement utilisée par un Spring MVC DomainObjectReader voir aussi: JsonPatchHandler

 @RepositoryRestController public class BookCustomRepository { private final DomainObjectReader domainObjectReader; private final ObjectMapper mapper; private final BookRepository repository; @Autowired public BookCustomRepository(BookRepository bookRepository, ObjectMapper mapper, PersistentEntities persistentEntities, Associations associationLinks) { this.repository = bookRepository; this.mapper = mapper; this.domainObjectReader = new DomainObjectReader(persistentEntities, associationLinks); } @PatchMapping(value = "/book/{id}", consumes = {MediaType.APPLICATION_JSON_UTF8_VALUE, MediaType.APPLICATION_JSON_VALUE}) public ResponseEntity patch(@PathVariable Ssortingng id, ServletServerHttpRequest request) throws IOException { Book entityToPatch = repository.findById(id).orElseThrow(ResourceNotFoundException::new); Book patched = domainObjectReader.read(request.getBody(), entityToPatch, mapper); repository.save(patched); return ResponseEntity.noContent().build(); } } 

J’ai corrigé le problème comme ça, parce que je ne peux pas changer le service

 public class Test { void updatePerson(Person person,PersonPatch patch) { for (PersonPatch.PersonPatchField updatedField : patch.updatedFields) { switch (updatedField){ case firstname: person.setFirstname(patch.getFirstname()); continue; case lastname: person.setLastname(patch.getLastname()); continue; case title: person.setTitle(patch.getTitle()); continue; } } } public static class PersonPatch { private final List updatedFields = new ArrayList(); public List updatedFields() { return updatedFields; } public enum PersonPatchField { firstname, lastname, title } private Ssortingng firstname; private Ssortingng lastname; private Ssortingng title; public Ssortingng getFirstname() { return firstname; } public void setFirstname(final Ssortingng firstname) { updatedFields.add(PersonPatchField.firstname); this.firstname = firstname; } public Ssortingng getLastname() { return lastname; } public void setLastname(final Ssortingng lastname) { updatedFields.add(PersonPatchField.lastname); this.lastname = lastname; } public Ssortingng getTitle() { return title; } public void setTitle(final Ssortingng title) { updatedFields.add(PersonPatchField.title); this.title = title; } } 

Jackson n’a appelé que lorsque des valeurs existent. Vous pouvez donc enregistrer quel setter a été appelé.

Voici une implémentation pour une commande de patch utilisant googles GSON.

 package de.tef.service.payment; import com.google.gson.*; class JsonHelper { static  T patch(T object, Ssortingng patch, Class clazz) { JsonElement o = new Gson().toJsonTree(object); JsonObject p = new JsonParser().parse(patch).getAsJsonObject(); JsonElement result = patch(o, p); return new Gson().fromJson(result, clazz); } static JsonElement patch(JsonElement object, JsonElement patch) { if (patch.isJsonArray()) { JsonArray result = new JsonArray(); object.getAsJsonArray().forEach(result::add); return result; } else if (patch.isJsonObject()) { System.out.println(object + " => " + patch); JsonObject o = object.getAsJsonObject(); JsonObject p = patch.getAsJsonObject(); JsonObject result = new JsonObject(); o.getAsJsonObject().entrySet().stream().forEach(e -> result.add(e.getKey(), p.get(e.getKey()) == null ? e.getValue() : patch(e.getValue(), p.get(e.getKey())))); return result; } else if (patch.isJsonPrimitive()) { return patch; } else if (patch.isJsonNull()) { return patch; } else { throw new IllegalStateException(); } } } 

L’implémentation est recursiv pour prendre soin des structures nestedes. Les tableaux ne sont pas fusionnés, car ils ne possèdent pas de clé pour la fusion.

Le “patch” JSON est directement converti de Ssortingng en JsonElement et non en un object pour garder les champs non remplis en dehors des champs remplis avec NULL.

Vous pouvez utiliser Optional<> pour cela:

 public class MyEntityUpdate { private Optional aVeryBigSsortingng; } 

De cette façon, vous pouvez inspecter l’object de mise à jour comme suit:

 if(update.getAVeryBigSsortingng() != null) entity.setAVeryBigSsortingng(update.getAVeryBigSsortingng().get()); 

Si le champ aVeryBigSsortingng n’est pas dans le document JSON, le champ POJO aVeryBigSsortingng sera null . S’il se trouve dans le document JSON, mais avec une valeur null , le champ POJO sera un champ Optional avec la valeur enveloppée null . Cette solution vous permet de faire la distinction entre les cas “sans mise à jour” et “à mettre à zéro”.