Aggiornamento parziale dei dati con Spring Data

1. Introduzione

Il salvataggio di CrudRespository di Spring Data è indubbiamente semplice, ma una caratteristica potrebbe essere uno svantaggio: aggiorna ogni colonna della tabella. Questa è la semantica della U in CRUD, ma cosa succede se invece volessimo fare un PATCH?

In questo tutorial, tratteremo le tecniche e gli approcci per eseguire un aggiornamento parziale anziché completo.

2. Problema

Come affermato in precedenza, save () sovrascriverà qualsiasi entità corrispondente con i dati forniti, il che significa che non possiamo fornire dati parziali. Ciò può diventare scomodo, soprattutto per oggetti più grandi con molti campi.

Se guardassimo un ORM, esistono alcune patch, come:

  • L' annotazione @DynamicUpdat e di Hibernate , che riscrive dinamicamente la query di aggiornamento
  • Annotazione @Column di JPA , poiché possiamo non consentire gli aggiornamenti su colonne specifiche utilizzando il parametro aggiornabile

Ma di seguito affronteremo questo problema con un intento specifico: il nostro scopo è preparare le nostre entità per il metodo di salvataggio senza fare affidamento su un ORM.

3. Il nostro caso

Per prima cosa, creiamo un'entità cliente :

@Entity public class Customer { @Id @GeneratedValue(strategy = GenerationType.AUTO) public long id; public String name; public String phone; } 

Quindi, definiamo un semplice repository CRUD:

@Repository public interface CustomerRepository extends CrudRepository { Customer findById(long id); }

Infine, prepariamo un CustomerService :

@Service public class CustomerService { @Autowired CustomerRepository repo; public void addCustomer(String name) { Customer c = new Customer(); c.name = name; repo.save(c); } }

4. Carica e salva l'approccio

Diamo prima un'occhiata a un approccio che probabilmente è familiare: caricare le nostre entità dal database e quindi aggiornare solo i campi di cui abbiamo bisogno.

Sebbene questo sia semplice e ovvio, è uno degli approcci più semplici che possiamo usare.

Aggiungiamo un metodo nel nostro servizio per aggiornare i dati di contatto dei nostri clienti.

public void updateCustomerContacts(long id, String phone) { Customer myCustomer = repo.findById(id); myCustomer.phone = phone; repo.save(myCustomer); }

Chiameremo il metodo findById e recupereremo l'entità corrispondente, quindi procediamo e aggiorneremo i campi richiesti e persisteremo i dati.

Questa tecnica di base è efficiente quando il numero di campi da aggiornare è relativamente piccolo e le nostre entità sono piuttosto semplici.

Cosa succederebbe con dozzine di campi da aggiornare?

4.1. Strategia di mappatura

Quando i nostri oggetti hanno un numero elevato di campi con diversi livelli di accesso, è abbastanza comune implementare il modello DTO.

Supponiamo ora di avere più di cento campi telefono nel nostro oggetto. Scrivere un metodo che trasferisca i dati da DTO alla nostra entità, come abbiamo fatto in precedenza, potrebbe essere fastidioso e piuttosto impossibile da mantenere.

Tuttavia, possiamo superare questo problema utilizzando una strategia di mappatura e in particolare con l' implementazione di MapStruct .

Creiamo un CustomerDto :

public class CustomerDto { private long id; public String name; public String phone; //... private String phone99; }

E anche un CustomerMapper :

@Mapper(componentModel = "spring") public interface CustomerMapper { void updateCustomerFromDto(CustomerDto dto, @MappingTarget Customer entity); }

L' annotazione @MappingTarget ci consente di aggiornare un oggetto esistente, salvandoci dal dolore di scrivere molto codice.

MapStruct ha un decoratore del metodo @BeanMapping , che ci consente di definire una regola per saltare i valori nulli durante il processo di mappatura. Aggiungiamolo alla nostra interfaccia del metodo updateCustomerFromDto :

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)

Con questo, possiamo caricare le entità memorizzate e unirle con un DTO prima di chiamare il metodo di salvataggio JPA : aggiorneremo infatti solo i valori modificati.

Quindi, aggiungiamo un metodo al nostro servizio, che chiamerà il nostro mappatore:

public void updateCustomer(CustomerDto dto) { Customer myCustomer = repo.findById(dto.id); mapper.updateCustomerFromDto(dto, myCustomer); repo.save(myCustomer); }

Lo svantaggio di questo approccio è che non possiamo passare valori null al database durante un aggiornamento.

4.2. Entità più semplici

Infine, tieni presente che possiamo affrontare questo problema dalla fase di progettazione di un'applicazione.

È essenziale definire le nostre entità il più piccole possibile.

Let's take a look at our Customer entity. What if we structure it a little bit, and extract all the phone fields to ContactPhone entities and be under a one-to-many relationship?

@Entity public class CustomerStructured { @Id @GeneratedValue(strategy = GenerationType.AUTO) public Long id; public String name; @OneToMany(fetch = FetchType.EAGER, targetEntity=ContactPhone.class, mappedBy="customerId") private List contactPhones; }

The code is clean and, more importantly, we achieved something. Now, we can update our entities without having to retrieve and fill all the phone data.

Handling small and bounded entities allows us to update only the necessary fields.

The only inconvenience of this approach is that we should design our entities with awareness, without falling into the trap of overengineering.

5. Custom Query

Another approach we can implement is to define a custom query for partial updates.

In fact, JPA defines two annotations, @Modifying and @Query, which allow us to write our update statement explicitly.

We can now tell our application how to behave during an update, without leaving the burden on the ORM.

Let's add our custom update method in the repository:

@Modifying @Query("update Customer u set u.phone = :phone where u.id = :id") void updatePhone(@Param(value = "id") long id, @Param(value = "phone") String phone); 

Now, we can rewrite our update method:

public void updateCustomerContacts(long id, String phone) { repo.updatePhone(id, phone); } 

Now we are able to perform a partial update: with just a few lines of code and without altering our entities we've achieved our goal.

The disadvantage of this technique is that we'll have to define a method for each possible partial update of our object.

6. Conclusion

The partial data update is quite a fundamental operation; while we can have our ORM to handle it, sometimes it could be profitable to get full control over it.

Come abbiamo visto, possiamo precaricare i nostri dati e quindi aggiornarli o definire le nostre istruzioni personalizzate, ma ricordati di essere consapevole degli svantaggi che questi approcci implicano e di come superarli.

Come al solito, il codice sorgente di questo articolo è disponibile su GitHub.