05
März
2017

POST einer OneToMany Resource mit Spring Data Rest

Wer möglichst schnell einen Microservice mit Persistenz aufgebaut haben möchte, der ist mit Spring Data Rest (in Kombination mit Spring Boot) gut bedient. Mit wenigen Annotationen an den Entities (Geschäftsobjekte) und Repositories ist eine voll funktionsfähige CRUD Anwendung einfach erstellt. Die Geschäftsobjekte können über eine vom Framework generierte RESTFUL API gelesen, geändert und persistiert werden. Bei komplexeren Abhängigkeiten zwischen den Geschäftsobjekten ist der generische Ansatz nicht mehr so einfach. Oft verfällt man dann in den Aufbau von eigenen RestControllern, um die Komplexität abbilden zu können. Im Folgenden untersuche ich kurz das Vorgehen, wie zwei voneinander abhängige Entitäten über die REST API in der Datenbank persistiert werden können, ohne einen eigenen Controller schreiben zu müssen.

Für unser Beispiel deklarieren wir analog dem Beispiel https://spring.io/guides/gs/accessing-data-rest/ eine Person und ihre OneToMany-Relation Adressen.

@Entity
@Table(name = "Person")
public class Person {
    @OneToMany(fetch = FetchType.EAGER)
    private Set<Adresse> adressen;

    @NotNull
    private String vorname;

    @NotNull
    private String nachname;
} 

(@Table definiert den Tabellennamen in der Datenbank. Wird diese Annotation weggelassen, wird der Tabellenname aus dem Klassennamen abgeleitet.)

@Entity
public class Adresse {
    @NotNull
    private String ort;

    @NotNull
    private String strasse;
} 

Für den Zugriff auf die Datenbank verwenden wir ein Repository je Tabelle. Durch die Annotation @RepositoryRestResource sind direkte Zugriffe per REST API möglich. Der URL-Pfad wird in der Annotation mit path hinterlegt.

@RepositoryRestResource(path = "adressen")
public interface AdressenRepository extends JpaRepository<Adresse, Long> {
} 

Durch das Erweitern von JpaRepository werden bereits alle notwendigen Methoden zum Abfragen und Speichern von Daten bereitgestellt. Benötige ich weitere kann ich diese durch Hinzufügen von Methoden im Interface erweitern. Aus den Methoden wird zur Laufzeit Code generiert, welcher die SQL Abfragen an die Datenbank durchführt. Diese Vorgehensweise beruht auf Namenskonventionen, welche in der Spring Data JPA Referenz ausführlich beschrieben sind.

@RepositoryRestResource(path = "personen")
public interface PersonenRepository extends JpaRepository<Person, Long> {    
    List<Person> findByNachname(String name);
} 

Mit dieser minimalistischen Implementierung, können jetzt die Personen aus der Datenbank per HTTP Get abgefragt werden.

Das war einfach: nun versuchen wir das Anlegen einer Person:

Versuch 1:  Auf den Pfad /personen wird ein JSON Dokument gepostet, welches die anzulegende Person beschreibt. Im Folgenden ein Testcase, der dies mit Hilfe eines RestTemplates durchführt. Der Test geht ohne Probleme durch, die Person wurde in der Datenbank angelegt (asserts und andere Unittest - Elemente wurden der Übersichtlichkeit halber weggelassen).

@Test    
public void testPostPerson() {
    Person person = new 
    person.setVorname("Donald");
    person.setNachname("Duck");

    String jsonPerson = util.toJson(person);
    HttpEntity<String> req = new HttpEntity<String>(jsonPerson, util.createHttpHeaders());

    String url = partnerApiEndPoint + "/personen";
    HttpEntity<String> response = 
              restTemplate.exchange(url, HttpMethod.POST, req, String.class);
}
 

Versuch 2:  Auf den Pfad /personen wird ein JSON Dokument gepostet, welches die anzulegende Person mit einer Adresse beschreibt.

@Test
public void testPostPersonMitAdresse() {
    
    Person person = new Person();
    person.setVorname("Donald");
    person.setNachname("Duck");

    Adresse adresse = new Adresse();
    adresse.setOrt("Entenhausen");
    adresse.setStrasse("Zum Geldspeicher");
    adresse.setStrasseNr("2");
    adresse.setPostleitzahl("11111");

    // adresse hinzufügen
    person.getAdressen().add(adresse);

    String json = util.toJson(person);
    // Post auf /personen
    HttpEntity<String> req  = new HttpEntity<String>(json, util.createHttpHeaders());
        
    String url = partnerApiEndPoint + "/personen";
    HttpEntity<String> resp = restTemplate.exchange(url, HttpMethod.POST, req, String.class);
} 

Dieser Versuch schlägt fehlt mit einer Exception.

Caused by: org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.net.URI] to type [de.cloudfactor.partner.model.adresse.Adresse] for value 'identifier'; nested exception is java.lang.IllegalArgumentException: Cannot resolve URI identifier. Is it local or remote? Only local URIs are resolvable.
    at org.springframework.data.rest.core.UriToEntityConverter.convert(UriToEntityConverter.java:119) ~[spring-data-rest-core-2.6.0.RELEASE.jar:na]
    at org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module$UriStringDeserializer.deserialize(PersistentEntityJackson2Module.java:521) ~[spring-data-rest-webmvc-2.6.0.RELEASE.jar:na]
    at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:287) ~[jackson-databind-2.8.6.jar:2.8.6]
    ... 103 common frames omitted
Caused by: java.lang.IllegalArgumentException: Cannot resolve URI identifier. Is it local or remote? Only local URIs are resolvable.
    ... 106 common frames omitted

 

Spring Data Rest ist nicht mehr in der Lage das JSON Dokument zu analysieren und die richtigen Repositories zur Speicherung zu erraten. Nun habe ich entweder die Möglichkeit einen eigenen Converter zu schreiben und einzuhängen (Alternative 1), oder einen eigenen Controller (Alternative 2), der in der Lage ist, Person und Adresse über eine Transaktion in beide Tabellen zu schreiben. Möchte ich aber bei meinem (code-)minimalistischen Ansatz bleiben, kann ich auch zunächst die Person und dann die Adresse anlegen. Danach erfolgt die Verlinkung der beiden Resourcen (Alternative 3). Alternative 2 ist wohl die gängigste und im Internet mehr als ausreichend beschrieben. Wir untersuchen daher nun die 3. Alternative.

Versuch 3: Verlinkung einer OneToMany Relation mit HTTP PUT:

// Adresse anlegen, analog Beispiel oben
...
// URL der angelegten Resource aus dem Location Header der Antwort abfragen 
URI adresslocation = responsePostAdresse.getHeaders().getLocation();

// Partner anlegen, analog Beispiel oben
...
// URL der angelegten Resource aus dem Location Header der Antwort abfragen
URI partnerlocation = partnerResponse.getHeaders().getLocation();

// Die beiden Resourcen verlinken

HttpHeaders headers = new HttpHeaders();
// dazu den Content Header auf "text/uri-list ändern
headers.add(HttpHeaders.CONTENT_TYPE, "text/uri-list");

// Inhalt des Requests ist die URL der zuvor angelegten Adresse
HttpEntity<String> linkRequest = new HttpEntity<String>(adresslocation.toString(), headers);

// Die Verlinkung erfolgt über HTTP PUT auf die Adressen URL des zuvor angelegten Partners
HttpEntity<String> linkResponse = restTemplate.exchange(partnerlocation + "/adressen", HttpMethod.PUT, linkRequest, String.class); 

Zum besseren Verständnis folgt hier der HTTP Mitschnitt:

1. Antwort nach Anlage der Adresse:

Response POST Adresse
<201 Created,{
  "ort" : "Entenhausen",
  "postleitzahl" : "11111",
  "strasse" : "Zum Geldspeicher",
  "strasseNr" : "2",
  "typ" : [ ],
  "_links" : {
    "self" : {
      "href" : "http://localhost:7777/partner/adressen/13"
    },
    "adresse" : {
      "href" : "http://localhost:7777/partner/adressen/13"
    }
  }
},{Access-Control-Allow-Origin=[*], Access-Control-Allow-Methods=[POST, PUT, GET, OPTIONS, DELETE], Access-Control-Allow-Headers=[x-requested-with], Access-Control-Max-Age=[3600], Access-Control-Expose-Headers=[X-AUTH-TOKEN], X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], X-Application-Context=[application:7777], ETag=["2017-03-05 20:15:35.338"], Location=[http://localhost:7777/partner/adressen/13], Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Sun, 05 Mar 2017 19:15:35 GMT]}> 

Location Adresse: http://localhost:7777/partner/adressen/13

2. Antwort nach Anlage der Person

Response Partner Anlage: 
<201 Created,{
  "anrede" : "Herr",
  "nachname" : "Duck",
  "notiz" : null,
  "typ" : [ ],
  "vorname" : "Donald",
  "_links" : {
    "self" : {
      "href" : "http://localhost:7777/partner/personen/14"
    },
    "person" : {
      "href" : "http://localhost:7777/partner/personen/14"
    },
    "adressen" : {
      "href" : "http://localhost:7777/partner/personen/14/adressen"
    }
  }
},{Access-Control-Allow-Origin=[*], Access-Control-Allow-Methods=[POST, PUT, GET, OPTIONS, DELETE], Access-Control-Allow-Headers=[x-requested-with], Access-Control-Max-Age=[3600], Access-Control-Expose-Headers=[X-AUTH-TOKEN], X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], X-Application-Context=[application:7777], ETag=["2017-03-05 20:18:27.899"], Location=[http://localhost:7777/partner/personen/14], Content-Type=[application/json;charset=UTF-8], Transfer-Encoding=[chunked], Date=[Sun, 05 Mar 2017 19:18:27 GMT]}> 

Location Partner: http://localhost:7777/partner/personen/14

3 a: Request Body mit HTTP Header "Content-Type" zum Verlinken

<http://localhost:7777/partner/adressen/13,
{Content-Type=[text/uri-list]}> 

3b: Post des Request auf http://localhost:7777/partner/personen/14/adressen

3c: Response auf den Verlinkungsrequest:

<204 No Content,{Access-Control-Allow-Origin=[*], Access-Control-Allow-Methods=[POST, PUT, GET, OPTIONS, DELETE], Access-Control-Allow-Headers=[x-requested-with], Access-Control-Max-Age=[3600], Access-Control-Expose-Headers=[X-AUTH-TOKEN], X-Content-Type-Options=[nosniff], X-XSS-Protection=[1; mode=block], Cache-Control=[no-cache, no-store, max-age=0, must-revalidate], Pragma=[no-cache], Expires=[0], X-Frame-Options=[DENY], X-Application-Context=[application:7777], Date=[Sun, 05 Mar 2017 19:27:08 GMT]}> 

Erfolg: Der Partner ist mit seiner Adresse angelegt und verlinkt. Zugegeben: das sind drei Requests gewesen. Je nach Anforderung mag das zuviel sein. Bei einem Offline-fähigen Angular Client, der seine angelegten Daten im Hintergrund synchronisiert sobald er eine Internet Verbindung hat, ist dieses Vorgehen hinreichend.

In einem folgenden Artikel untersuche ich, wie dieses Muster in Angular als Client implementiert wird. Bis dahin ..  Happy Hacking !

Author; Dirk Lammers Categories: Spring

About the Author

Dirk Lammers

Dirk Lammers

Software Architekt @ lvm.de

Software Developer | Spring/Java | Angular/JavaScript

Projektveteran aus unzähligen & weltweiten IT-Beratungsprojekten seit 1996

 

IBM Architecture Certification     The Open Group Master IT Architect

 

 

Comments (1)

Leave a comment

You are commenting as guest.