Arbeiten mit Beziehungen in Spring Data REST

Arbeiten mit Beziehungen in Spring Data REST

1. Überblick

In diesem Artikel werfen wir einen Blick aufhow to work with relationships between entities in Spring Data REST.

Wir werden uns auf die Assoziationsressourcen konzentrieren, die Spring Data REST für ein Repository bereitstellt, und dabei jeden Typ von Beziehung berücksichtigen, der definiert werden kann.

Um zusätzliche Einstellungen zu vermeiden, verwenden wir für die Beispiele die eingebettete DatenbankH2. Die Liste der erforderlichen Abhängigkeiten finden Sie in unserem Artikel zuIntroduction to Spring Data REST.

 

Und wenn Sie auf die erstenget started with Spring Data REST schauen, ist hier ein guter Weg, um den ersten Schritt zu machen:

[.iframe-fluid] ##

2. Eins-zu-eins-Beziehung

2.1. Das Datenmodell

Definieren wir zwei EntitätsklassenLibrary undAddress mit einer Eins-zu-Eins-Beziehung unter Verwendung der Annotation@OneToOne. Der Verein gehört demLibrary-Ende des Vereins:

@Entity
public class Library {

    @Id
    @GeneratedValue
    private long id;

    @Column
    private String name;

    @OneToOne
    @JoinColumn(name = "address_id")
    @RestResource(path = "libraryAddress", rel="address")
    private Address address;

    // standard constructor, getters, setters
}
@Entity
public class Address {

    @Id
    @GeneratedValue
    private long id;

    @Column(nullable = false)
    private String location;

    @OneToOne(mappedBy = "address")
    private Library library;

    // standard constructor, getters, setters
}

Die Annotation@RestResource ist optional und kann zum Anpassen des Endpunkts verwendet werden.

We must be careful to have different names for each association resource. Andernfalls wird einJsonMappingException mit der Meldung“Detected multiple association links with same relation type! Disambiguate association” angezeigt.

Der Zuordnungsname ist standardmäßig der Eigenschaftsname und kann mithilfe des Attributsrelder Annotation@RestResourceangepasst werden:

@OneToOne
@JoinColumn(name = "secondary_address_id")
@RestResource(path = "libraryAddress", rel="address")
private Address secondaryAddress;

Wenn wir diesecondaryAddress-Eigenschaft oben zurLibrary-Klasse hinzufügen würden, hätten wir zwei Ressourcen mit dem Namenaddress und würden auf einen Konflikt stoßen.

Wir können dies beheben, indem wir einen anderen Wert für das Attributrel angeben oder die AnnotationRestResourceweglassen, sodass der Ressourcenname standardmäßigsecondaryAddress lautet.

2.2. Die Repositories

Umexpose these entities as resources zu erhalten, erstellen wir für jede von ihnen zwei Repository-Schnittstellen, indem wir die SchnittstelleCrudRepository erweitern:

public interface LibraryRepository extends CrudRepository {}
public interface AddressRepository extends CrudRepository {}

2.3. Ressourcen erstellen

Fügen wir zunächst eineLibrary-Instanz hinzu, mit der gearbeitet werden soll:

curl -i -X POST -H "Content-Type:application/json"
  -d '{"name":"My Library"}' http://localhost:8080/libraries

Die API gibt das JSON-Objekt zurück:

{
  "name" : "My Library",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/libraries/1"
    },
    "library" : {
      "href" : "http://localhost:8080/libraries/1"
    },
    "address" : {
      "href" : "http://localhost:8080/libraries/1/libraryAddress"
    }
  }
}

Beachten Sie, dass Sie bei Verwendung voncurl unter Windows das doppelte Anführungszeichen inString, das den Körper vonJSONdarstellt, maskieren müssen:

-d "{\"name\":\"My Library\"}"

Wir können im Antworttext sehen, dass eine Zuordnungsressource am Endpunkt vonlibraries/{libraryId}/addressverfügbar gemacht wurde.

Bevor wir eine Zuordnung erstellen, gibt das Senden einer GET-Anforderung an diesen Endpunkt ein leeres Objekt zurück.

Wenn wir jedoch eine Zuordnung hinzufügen möchten, müssen wir zuerst auch eineAddress-Instanz erstellen:

curl -i -X POST -H "Content-Type:application/json"
  -d '{"location":"Main Street nr 5"}' http://localhost:8080/addresses

Das Ergebnis der POST-Anforderung ist ein JSON-Objekt, das den DatensatzAddressenthält:

{
  "location" : "Main Street nr 5",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/addresses/1"
    },
    "address" : {
      "href" : "http://localhost:8080/addresses/1"
    },
    "library" : {
      "href" : "http://localhost:8080/addresses/1/library"
    }
  }
}

2.4. Assoziationen erstellen

After persisting both instances, we can establish the relationship by using one of the association resources.

Dies erfolgt mithilfe der HTTP-Methode PUT, die einen Medientyp vontext/uri-list unterstützt, und eines Körpers, der dieURI der Ressource enthält, die an die Zuordnung gebunden werden soll.

Da die EntitätLibraryder Eigentümer der Zuordnung ist, fügen wir einer Bibliothek eine Adresse hinzu:

curl -i -X PUT -d "http://localhost:8080/addresses/1"
  -H "Content-Type:text/uri-list" http://localhost:8080/libraries/1/libraryAddress

Bei Erfolg wird der Status 204 zurückgegeben. Überprüfen Sie zur Überprüfung die Zuordnungsressourcelibraryderaddress:

curl -i -X GET http://localhost:8080/addresses/1/library

Dies sollte das JSON-ObjektLibrary mit dem Namen“My Library” zurückgeben.

Zuremove an association können wir den Endpunkt mit der DELETE-Methode aufrufen und dabei sicherstellen, dass die Zuordnungsressource des Eigentümers der Beziehung verwendet wird:

curl -i -X DELETE http://localhost:8080/libraries/1/libraryAddress

3. Eins-zu-viele-Beziehung

Eine Eins-zu-Viele-Beziehung wird mithilfe der Annotationen@OneToMany und@ManyToOne definiert und kann die optionale Annotation@RestResource enthalten, um die Zuordnungsressource anzupassen.

3.1. Das Datenmodell

Um eine Eins-zu-Viele-Beziehung zu veranschaulichen, fügen wir eine neueBook-Entität hinzu, die das "viele" -Ende einer Beziehung mit derLibrary-Entität darstellt:

@Entity
public class Book {

    @Id
    @GeneratedValue
    private long id;

    @Column(nullable=false)
    private String title;

    @ManyToOne
    @JoinColumn(name="library_id")
    private Library library;

    // standard constructor, getter, setter
}

Fügen wir die Beziehung auch zur KlasseLibraryhinzu:

public class Library {

    //...

    @OneToMany(mappedBy = "library")
    private List books;

    //...

}

3.2. Das Repository

Wir müssen auch einBookRepository erstellen:

public interface BookRepository extends CrudRepository { }

3.3. Die Ressourcen der Vereinigung

Umadd a book to a library zu erhalten, müssen wir zuerst eineBook-Instanz erstellen, indem wir die Erfassungsressource /booksverwenden:

curl -i -X POST -d "{\"title\":\"Book1\"}"
  -H "Content-Type:application/json" http://localhost:8080/books

Und hier ist die Antwort von der POST-Anfrage:

{
  "title" : "Book1",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/books/1"
    },
    "book" : {
      "href" : "http://localhost:8080/books/1"
    },
    "bookLibrary" : {
      "href" : "http://localhost:8080/books/1/library"
    }
  }
}

Im Antworttext sehen wir, dass der Zuordnungsendpunkt/books/{bookId}/library erstellt wurde.

Lassen Sie unsassociate the book with the library erstellen, die im vorherigen Abschnitt erstellt wurden, indem Sie eine PUT-Anforderung an die Zuordnungsressource senden, dieURI der Bibliotheksressource enthält:

curl -i -X PUT -H "Content-Type:text/uri-list"
-d "http://localhost:8080/libraries/1" http://localhost:8080/books/1/library

Wir könnenverify the books in the library mithilfe der GET-Methode für die Zuordnungsressource /booksder Bibliothek verwenden:

curl -i -X GET http://localhost:8080/libraries/1/books

Das zurückgegebene JSON-Objekt enthält einbooks-Array:

{
  "_embedded" : {
    "books" : [ {
      "title" : "Book1",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/1"
        },
        "book" : {
          "href" : "http://localhost:8080/books/1"
        },
        "bookLibrary" : {
          "href" : "http://localhost:8080/books/1/library"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/libraries/1/books"
    }
  }
}

Fürremove an association können wir die DELETE-Methode für die Zuordnungsressource verwenden:

curl -i -X DELETE http://localhost:8080/books/1/library

4. Viele-zu-Viele-Beziehung

Eine Viele-zu-Viele-Beziehung wird mithilfe der Annotation@ManyToManydefiniert, zu der wir@RestResource hinzufügen können.

4.1. Das Datenmodell

Um ein Beispiel für eine Viele-zu-Viele-Beziehung zu erstellen, fügen wir eine neue ModellklasseAuthorhinzu, die eine Viele-zu-Viele-Beziehung zur EntitätBookhat:

@Entity
public class Author {

    @Id
    @GeneratedValue
    private long id;

    @Column(nullable = false)
    private String name;

    @ManyToMany(cascade = CascadeType.ALL)
    @JoinTable(name = "book_author",
      joinColumns = @JoinColumn(name = "book_id", referencedColumnName = "id"),
      inverseJoinColumns = @JoinColumn(name = "author_id",
      referencedColumnName = "id"))
    private List books;

    //standard constructors, getters, setters
}

Fügen wir die Zuordnung auch in die KlasseBookein:

public class Book {

    //...

    @ManyToMany(mappedBy = "books")
    private List authors;

    //...
}

4.2. Das Repository

Erstellen wir eine Repository-Schnittstelle, um die EntitätAuthorzu verwalten:

public interface AuthorRepository extends CrudRepository { }

4.3. Die Ressourcen der Vereinigung

Wie in den vorherigen Abschnitten müssen wir zuerstcreate the resources eingeben, bevor wir die Zuordnung herstellen können.

Erstellen Sie zunächst eineAuthor-Instanz, indem Sie POST-Anforderungen an die Erfassungsressource von /authorsenden:

curl -i -X POST -H "Content-Type:application/json"
  -d "{\"name\":\"author1\"}" http://localhost:8080/authors

Als nächstes fügen wir unserer Datenbank einen zweitenBook-Datensatz hinzu:

curl -i -X POST -H "Content-Type:application/json"
  -d "{\"title\":\"Book 2\"}" http://localhost:8080/books

Führen Sie eine GET-Anforderung für unserenAuthor-Datensatz aus, um die Zuordnungs-URL anzuzeigen:

{
  "name" : "author1",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/authors/1"
    },
    "author" : {
      "href" : "http://localhost:8080/authors/1"
    },
    "books" : {
      "href" : "http://localhost:8080/authors/1/books"
    }
  }
}

Jetzt können wircreate an association zwischen den beidenBook-Datensätzen und demAuthor-Datensatz unter Verwendung des Endpunktsauthors/1/books mit der PUT-Methode, die einen Medientyp vontext/uri-list unterstützt, und can mehr als einURI erhalten.

Um mehrereURIs zu senden, müssen wir sie durch einen Zeilenumbruch trennen:

curl -i -X PUT -H "Content-Type:text/uri-list"
  --data-binary @uris.txt http://localhost:8080/authors/1/books

Die Dateiuris.txt enthält dieURIs der Bücher, jeweils in einer separaten Zeile:

http://localhost:8080/books/1
http://localhost:8080/books/2

Anverify both books have been associated with the author können wir eine GET-Anforderung an den Zuordnungsendpunkt senden:

curl -i -X GET http://localhost:8080/authors/1/books

Und wir erhalten diese Antwort:

{
  "_embedded" : {
    "books" : [ {
      "title" : "Book 1",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/1"
        }
      //...
      }
    }, {
      "title" : "Book 2",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/books/2"
        }
      //...
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/authors/1/books"
    }
  }
}

Anremove an association können wir eine Anforderung mit der DELETE-Methode an die URL der Zuordnungsressource senden, gefolgt von{bookId}:

curl -i -X DELETE http://localhost:8080/authors/1/books/1

5. Testen der Endpunkte mitTestRestTemplate

Erstellen wir eine Testklasse, die die Instanz vonTestRestTemplateeinfügt und die Konstanten definiert, die wir verwenden werden:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringDataRestApplication.class,
  webEnvironment = WebEnvironment.DEFINED_PORT)
public class SpringDataRelationshipsTest {

    @Autowired
    private TestRestTemplate template;

    private static String BOOK_ENDPOINT = "http://localhost:8080/books/";
    private static String AUTHOR_ENDPOINT = "http://localhost:8080/authors/";
    private static String ADDRESS_ENDPOINT = "http://localhost:8080/addresses/";
    private static String LIBRARY_ENDPOINT = "http://localhost:8080/libraries/";

    private static String LIBRARY_NAME = "My Library";
    private static String AUTHOR_NAME = "George Orwell";
}

5.1. Testen der Eins-zu-Eins-Beziehung

Erstellen wir eine@Test-Methode, mit derLibrary- undAddress-Objekte gespeichert werden, indem POST-Anforderungen an die Erfassungsressourcen gesendet werden.

Anschließend wird die Beziehung mit einer PUT-Anforderung an die Zuordnungsressource gespeichert und überprüft, ob sie mit einer GET-Anforderung an dieselbe Ressource hergestellt wurde:

@Test
public void whenSaveOneToOneRelationship_thenCorrect() {
    Library library = new Library(LIBRARY_NAME);
    template.postForEntity(LIBRARY_ENDPOINT, library, Library.class);

    Address address = new Address("Main street, nr 1");
    template.postForEntity(ADDRESS_ENDPOINT, address, Address.class);

    HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.add("Content-type", "text/uri-list");
    HttpEntity httpEntity
      = new HttpEntity<>(ADDRESS_ENDPOINT + "/1", requestHeaders);
    template.exchange(LIBRARY_ENDPOINT + "/1/libraryAddress",
      HttpMethod.PUT, httpEntity, String.class);

    ResponseEntity libraryGetResponse
      = template.getForEntity(ADDRESS_ENDPOINT + "/1/library", Library.class);
    assertEquals("library is incorrect",
      libraryGetResponse.getBody().getName(), LIBRARY_NAME);
}

5.2. Testen der Eins-zu-Viele-Beziehung

Erstellen wir eine@Test-Methode, die eineLibrary-Instanz und zweiBook-Instanzen speichert, eine PUT-Anforderung an die/library-Zuordnungsressource jedesBook-Objekts sendet und diese überprüft Die Beziehung wurde gespeichert:

@Test
public void whenSaveOneToManyRelationship_thenCorrect() {
    Library library = new Library(LIBRARY_NAME);
    template.postForEntity(LIBRARY_ENDPOINT, library, Library.class);

    Book book1 = new Book("Dune");
    template.postForEntity(BOOK_ENDPOINT, book1, Book.class);

    Book book2 = new Book("1984");
    template.postForEntity(BOOK_ENDPOINT, book2, Book.class);

    HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.add("Content-Type", "text/uri-list");
    HttpEntity bookHttpEntity
      = new HttpEntity<>(LIBRARY_ENDPOINT + "/1", requestHeaders);
    template.exchange(BOOK_ENDPOINT + "/1/library",
      HttpMethod.PUT, bookHttpEntity, String.class);
    template.exchange(BOOK_ENDPOINT + "/2/library",
      HttpMethod.PUT, bookHttpEntity, String.class);

    ResponseEntity libraryGetResponse =
      template.getForEntity(BOOK_ENDPOINT + "/1/library", Library.class);
    assertEquals("library is incorrect",
      libraryGetResponse.getBody().getName(), LIBRARY_NAME);
}

5.3. Testen der Viele-zu-Viele-Beziehung

Zum Testen der Viele-zu-Viele-Beziehung zwischenBook undAuthorEntitäten erstellen wir eine Testmethode, mit der einAuthor-Datensatz und zweiBook-Datensätze gespeichert werden.

Anschließend sendet es eine PUT-Anforderung an die Zuordnungsressource/booksmit den beidenBooksURIs und überprüft, ob die Beziehung hergestellt wurde:

@Test
public void whenSaveManyToManyRelationship_thenCorrect() {
    Author author1 = new Author(AUTHOR_NAME);
    template.postForEntity(AUTHOR_ENDPOINT, author1, Author.class);

    Book book1 = new Book("Animal Farm");
    template.postForEntity(BOOK_ENDPOINT, book1, Book.class);

    Book book2 = new Book("1984");
    template.postForEntity(BOOK_ENDPOINT, book2, Book.class);

    HttpHeaders requestHeaders = new HttpHeaders();
    requestHeaders.add("Content-type", "text/uri-list");
    HttpEntity httpEntity = new HttpEntity<>(
      BOOK_ENDPOINT + "/1\n" + BOOK_ENDPOINT + "/2", requestHeaders);
    template.exchange(AUTHOR_ENDPOINT + "/1/books",
      HttpMethod.PUT, httpEntity, String.class);

    String jsonResponse = template
      .getForObject(BOOK_ENDPOINT + "/1/authors", String.class);
    JSONObject jsonObj = new JSONObject(jsonResponse).getJSONObject("_embedded");
    JSONArray jsonArray = jsonObj.getJSONArray("authors");
    assertEquals("author is incorrect",
      jsonArray.getJSONObject(0).getString("name"), AUTHOR_NAME);
}

6. Fazit

In diesem Tutorial haben wir die Verwendung verschiedener Arten von Beziehungen mit Spring Data REST demonstriert.

Der vollständige Quellcode der Beispiele istover on GitHub.