Partielle Datenaktualisierung mit Federdaten

1. Einleitung

Das Speichern von CrudRespository # von Spring Data ist zweifellos einfach, aber eine Funktion könnte ein Nachteil sein: Es aktualisiert jede Spalte in der Tabelle. Dies ist die Semantik des U in CRUD, aber was ist, wenn wir stattdessen einen PATCH durchführen möchten?

In diesem Tutorial werden Techniken und Ansätze zur Durchführung eines teilweisen statt eines vollständigen Updates behandelt.

2. Problem

Wie bereits erwähnt, überschreibt save () jede übereinstimmende Entität mit den bereitgestellten Daten, was bedeutet, dass wir keine Teildaten liefern können. Dies kann insbesondere bei größeren Objekten mit vielen Feldern unpraktisch werden.

Wenn wir uns ein ORM ansehen würden, existieren einige Patches wie:

  • Die @ DynamicUpdat e-Annotation von Hibernate , mit der die Aktualisierungsabfrage dynamisch neu geschrieben wird
  • Die @ Column- Annotation von JPA , da wir Aktualisierungen für bestimmte Spalten mithilfe des aktualisierbaren Parameters nicht zulassen können

Im Folgenden werden wir dieses Problem jedoch mit besonderer Absicht angehen: Unser Ziel ist es, unsere Entitäten auf die Speichermethode vorzubereiten , ohne sich auf ein ORM zu verlassen.

3. Unser Fall

Lassen Sie uns zunächst eine Kundenentität erstellen :

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

Dann definieren wir ein einfaches CRUD-Repository:

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

Schließlich bereiten wir einen Kundenservice vor :

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

4. Ansatz laden und speichern

Schauen wir uns zunächst einen Ansatz an, der wahrscheinlich bekannt ist: Laden Sie unsere Entitäten aus der Datenbank und aktualisieren Sie dann nur die Felder, die wir benötigen.

Obwohl dies einfach und offensichtlich ist, ist es einer der einfachsten Ansätze, die wir verwenden können.

Fügen wir unserem Service eine Methode hinzu, um die Kontaktdaten unserer Kunden zu aktualisieren.

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

Wir rufen die findById- Methode auf und rufen die übereinstimmende Entität ab. Anschließend aktualisieren wir die erforderlichen Felder und behalten die Daten bei.

Diese grundlegende Technik ist effizient, wenn die Anzahl der zu aktualisierenden Felder relativ gering ist und unsere Entitäten recht einfach sind.

Was würde mit Dutzenden von zu aktualisierenden Feldern passieren?

4.1. Kartierungsstrategie

Wenn unsere Objekte eine große Anzahl von Feldern mit unterschiedlichen Zugriffsebenen haben, ist es durchaus üblich, das DTO-Muster zu implementieren.

Angenommen, wir haben mehr als hundert Telefonfelder in unserem Objekt. Das Schreiben einer Methode, mit der die Daten von DTO wie zuvor an unsere Entität übertragen werden, kann lästig und ziemlich wartbar sein.

Trotzdem können wir dieses Problem mithilfe einer Mapping-Strategie und speziell mit der MapStruct- Implementierung lösen .

Erstellen wir ein CustomerDto :

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

Und auch ein CustomerMapper :

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

Mit der Annotation @MappingTarget können wir ein vorhandenes Objekt aktualisieren, sodass wir nicht viel Code schreiben müssen.

MapStruct verfügt über einen @ BeanMapping- Methodendekorator, mit dem wir eine Regel definieren können, mit der Nullwerte während des Zuordnungsprozesses übersprungen werden. Fügen wir es unserer updateCustomerFromDto- Methodenschnittstelle hinzu:

@BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)

Auf diese Weise können wir gespeicherte Entitäten laden und mit einem DTO zusammenführen, bevor wir die JPA- Speichermethode aufrufen. Tatsächlich aktualisieren wir nur die geänderten Werte.

Fügen wir also unserem Service eine Methode hinzu, die unseren Mapper aufruft:

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

Der Nachteil dieses Ansatzes besteht darin, dass wir während eines Updates keine Nullwerte an die Datenbank übergeben können.

4.2. Einfachere Entitäten

Denken Sie zum Schluss daran, dass wir dieses Problem bereits in der Entwurfsphase einer Anwendung angehen können.

Es ist wichtig, unsere Einheiten so klein wie möglich zu definieren.

Werfen wir einen Blick auf unsere Kundeneinheit . Was ist, wenn wir es ein wenig strukturieren und alle Telefonfelder in ContactPhone- Entitäten extrahieren und eine Eins-zu-Viele-Beziehung eingehen ?

@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; }

Der Code ist sauber und vor allem haben wir etwas erreicht. Jetzt können wir unsere Entitäten aktualisieren, ohne alle Telefondaten abrufen und füllen zu müssen .

Durch den Umgang mit kleinen und begrenzten Entitäten können wir nur die erforderlichen Felder aktualisieren.

Die einzige Unannehmlichkeit dieses Ansatzes besteht darin, dass wir unsere Entitäten bewusst gestalten sollten, ohne in die Falle der Überentwicklung zu geraten.

5. Benutzerdefinierte Abfrage

Ein weiterer Ansatz, den wir implementieren können, besteht darin, eine benutzerdefinierte Abfrage für Teilaktualisierungen zu definieren.

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.

Wie wir gesehen haben, können wir unsere Daten vorab laden und dann aktualisieren oder unsere benutzerdefinierten Anweisungen definieren. Beachten Sie jedoch, welche Nachteile diese Ansätze mit sich bringen und wie Sie diese überwinden können.

Wie üblich ist der Quellcode für diesen Artikel auf GitHub verfügbar.