2016-07-17 127 views
6

Sto provando a distinguere tra valori nulli e valori non forniti quando si aggiorna parzialmente un'entità con il metodo di richiesta PUT in Spring Rest Controller.Come distinguere tra valori null e non forniti per gli aggiornamenti parziali nel controller Spring Rest

Si consideri il seguente entità, come ad esempio:

@Entity 
private class Person { 

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

    /* let's assume the following attributes may be null */ 
    private String firstName; 
    private String lastName; 

    /* getters and setters ... */ 
} 

mio repository persona (primavera dati):

@Repository 
public interface PersonRepository extends CrudRepository<Person, Long> { 
} 

Il DTO che uso:

private class PersonDTO { 
    private String firstName; 
    private String lastName; 

    /* getters and setters ... */ 
} 

la mia primavera RestController :

@RestController 
@RequestMapping("/api/people") 
public class PersonController { 

    @Autowired 
    private PersonRepository people; 

    @Transactional 
    @RequestMapping(path = "/{personId}", method = RequestMethod.PUT) 
    public ResponseEntity<?> update(
      @PathVariable String personId, 
      @RequestBody PersonDTO dto) { 

     // get the entity by ID 
     Person p = people.findOne(personId); // we assume it exists 

     // update ONLY entity attributes that have been defined 
     if(/* dto.getFirstName is defined */) 
      p.setFirstName = dto.getFirstName; 

     if(/* dto.getLastName is defined */) 
      p.setLastName = dto.getLastName; 

     return ResponseEntity.ok(p); 
    } 
} 

richiesta con la proprietà mancante

{"firstName": "John"} 

Comportamento previsto: aggiornamento firstName= "John" (lasciare lastName invariato).

richiesta con la proprietà nullo

{"firstName": "John", "lastName": null} 

Comportamento previsto: aggiornamento firstName="John" e impostare lastName=null.

non riesco a distinguere tra questi due casi, poiché lastName in DTO è sempre impostato su null da Jackson.

Nota: So che le best practice REST (RFC 6902) raccomandano l'uso di PATCH invece di PUT per gli aggiornamenti parziali, ma nel mio particolare scenario ho bisogno di usare PUT.

+0

Eventuali duplicati di [@RequestBody come differenziare un valore non inviato da un valore nullo?] (Https://stackoverflow.com/questions/40610604/requestbody-how-to-differentiate-an-unsent-value -from-a-null-value) – laffuste

risposta

1

In realtà, se si ignora la convalida, è possibile risolvere il problema in questo modo.

public class BusDto { 
     private Map<String, Object> changedAttrs = new HashMap<>(); 

     /* getter and setter */ 
    } 
  • In primo luogo, scrivere una classe eccellente per la vostra dto, come BusDto.
  • In secondo luogo, modificare la dto di estendere la classe super, e cambiare metodo set del del dto, di mettere il nome dell'attributo e il valore ai changedAttrs (beacause la molla sarebbe richiamare il set quando l'attributo ha valore non importa nulla o non nullo).
  • In terzo luogo, attraversare la mappa.
1

Ho provato a risolvere lo stesso problema. Ho trovato abbastanza facile usare JsonNode come DTO. In questo modo ottieni solo ciò che viene inviato.

Sarà necessario scrivere uno MergeService che esegue il lavoro effettivo, simile a BeanWrapper. Non ho trovato un framework esistente che possa fare esattamente ciò che è necessario. (Se si utilizzano solo richieste JSON, è possibile utilizzare il metodo Jacksons readForUpdate.)

In realtà usiamo un altro tipo di nodo in quanto abbiamo bisogno della stessa funzionalità da "invio moduli standard" e altre chiamate di servizio. Inoltre le modifiche dovrebbero essere applicate all'interno di una transazione all'interno di qualcosa chiamato EntityService.

Questo MergeService sarà purtroppo diventare molto complesso, in quanto sarà necessario per gestire le proprietà, liste, insiemi e mappe da soli :)

Il pezzo più problematico per me è stato quello di distinguere tra i cambiamenti all'interno di un elemento di una lista/set e modifiche o sostituzioni di liste/set.

E anche la validazione non sarà facile in quanto è necessario validare alcune proprietà nei confronti di un altro modello (le entità JPA nel mio caso)

EDIT - Alcuni codice di mappatura (pseudo-codice):

class SomeController { 
    @RequestMapping(value = { "/{id}" }, method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE) 
    @ResponseBody 
    public void save(
      @PathVariable("id") final Integer id, 
      @RequestBody final JsonNode modifications) { 
     modifierService.applyModifications(someEntityLoadedById, modifications); 
    } 
} 

class ModifierService { 

    public void applyModifications(Object updateObj, JsonNode node) 
      throws Exception { 

     BeanWrapperImpl bw = new BeanWrapperImpl(updateObj); 
     Iterator<String> fieldNames = node.fieldNames(); 

     while (fieldNames.hasNext()) { 
      String fieldName = fieldNames.next(); 
      Object valueToBeUpdated = node.get(fieldName); 
      Class<?> propertyType = bw.getPropertyType(fieldName); 
      if (propertyType == null) { 
       if (!ignoreUnkown) { 
        throw new IllegalArgumentException("Unkown field " + fieldName + " on type " + bw.getWrappedClass()); 
       } 
      } else if (Map.class.isAssignableFrom(propertyType)) { 
        handleMap(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects); 
      } else if (Collection.class.isAssignableFrom(propertyType)) { 
        handleCollection(bw, fieldName, valueToBeUpdated, ModificationType.MODIFY, createdObjects); 
      } else { 
        handleObject(bw, fieldName, valueToBeUpdated, propertyType, createdObjects); 
      } 
     } 
    } 
} 
+0

La tua risposta sembra interessante, ma non ho capito molto bene. Potresti per favore esaltarlo con un esempio pertinente? –

+0

Quale parte, jsonnode requestmapping, il mergeservice o la convalida? –

+0

Non è chiaro come utilizzare JsonNode nel mio metodo di aggiornamento nella classe Controller. I miei DTO dovrebbero ereditare da JsonNode? –

0

Forse troppo tardi per una risposta, ma si potrebbe:

  • per impostazione predefinita, fare valori non unset 'null'. Fornire un elenco esplicito tramite parametri di query quali campi si desidera disinserire. In tal modo è comunque possibile inviare JSON che corrisponde alla propria entità e avere la flessibilità di disinserire i campi quando necessario.

  • A seconda del caso d'uso, alcuni endpoint possono trattare esplicitamente tutti i valori nulli come operazioni non impostate. Un po 'pericoloso per le patch, ma in alcune circostanze potrebbe essere un'opzione.

0

Utilizzare flag booleani come jackson's author recommends.

class PersonDTO { 
    private String firstName; 
    private boolean isFirstNameDirty; 

    public void setFirstName(String firstName){ 
     this.firstName = firstName; 
     this.isFirstNameDirty = true; 
    } 

    public void getFirstName() { 
     return firstName; 
    } 

    public boolean hasFirstName() { 
     return isFirstNameDirty; 
    } 
} 
Problemi correlati