web-dev-qa-db-fra.com

Comment maintenir des relations bidirectionnelles avec Spring Data REST et JPA?

En travaillant avec Spring Data REST, si vous avez une relation OneToMany ou ManyToOne, l'opération PUT renvoie 200 sur l'entité "non propriétaire" mais ne persiste pas réellement la ressource jointe.

Exemples d'entités:

@Entity(name = 'author')
@ToString
class AuthorEntity implements Author {

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

    String fullName

    @ManyToMany(mappedBy = 'authors')
    Set<BookEntity> books
}


@Entity(name = 'book')
@EqualsAndHashCode
class BookEntity implements Book {

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

    @Column(nullable = false)
    String title

    @Column(nullable = false)
    String isbn

    @Column(nullable = false)
    String publisher

    @ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
    Set<AuthorEntity> authors
}

Si vous les sauvegardez avec un PagingAndSortingRepository, vous pouvez OBTENIR un Book, suivre le lien authors sur le livre et faire un PUT avec l'URI d'un auteur à associer. Vous ne pouvez pas aller dans l'autre sens.

Si vous effectuez un GET sur un auteur et effectuez un PUT sur son lien books, la réponse renvoie 200, mais la relation n'est jamais persistante.

Est-ce le comportement attendu?

27
keaplogik

tl; dr

La clé n'est pas tellement quelque chose dans Spring Data REST - car vous pouvez facilement le faire fonctionner dans votre scénario - mais en vous assurant que votre modèle garde les deux extrémités de l'association synchronisées.

Le problème

Le problème que vous voyez ici provient du fait que Spring Data REST modifie fondamentalement la propriété books de votre AuthorEntity. Cela ne reflète pas cette mise à jour dans la propriété authors de la BookEntity. Cela doit être contourné manuellement, ce qui n'est pas une contrainte que Spring Data REST constitue mais la façon dont JPA Vous pourrez reproduire le comportement erroné en invoquant simplement les setters manuellement et en essayant de conserver le résultat.

Comment résoudre ça?

Si la suppression de l'association bidirectionnelle n'est pas une option (voir ci-dessous pourquoi je recommanderais cela), la seule façon de faire ce travail est de s'assurer que les modifications apportées à l'association sont reflétées des deux côtés. Habituellement, les gens s'en occupent en ajoutant manuellement l'auteur au BookEntity lorsqu'un livre est ajouté:

class AuthorEntity {

  void add(BookEntity book) {

    this.books.add(book);

    if (!book.getAuthors().contains(this)) {
       book.add(this);
    }
  }
}

La clause if supplémentaire devrait également être ajoutée du côté BookEntity si vous voulez vous assurer que les modifications de l'autre côté sont également propagées. Le if est fondamentalement requis car sinon les deux méthodes s'appelleraient constamment.

Spring Data REST, utilise par défaut l'accès aux champs pour qu'il n'y ait en fait aucune méthode dans laquelle vous pouvez mettre cette logique. Une option serait de passer à l'accès à la propriété et de mettre la logique dans les setters. Une autre option consiste à utiliser une méthode annotée avec @PreUpdate/@PrePersist Qui itère sur les entités et s'assure que les modifications sont reflétées des deux côtés.

Suppression de la cause première du problème

Comme vous pouvez le voir, cela ajoute beaucoup de complexité au modèle de domaine. Comme j'ai plaisanté sur Twitter hier:

Règle n ° 1 des associations bidirectionnelles: ne pas les utiliser… :)

Cela simplifie généralement le problème si vous essayez de ne pas utiliser la relation bidirectionnelle dans la mesure du possible et plutôt de vous rabattre sur un référentiel pour obtenir toutes les entités qui composent l'arrière de l'association.

Une bonne heuristique pour déterminer quel côté couper est de penser quel côté de l'association est vraiment central et crucial pour le domaine que vous modélisez. Dans votre cas, je dirais qu'il est parfaitement normal qu'un auteur existe sans livres écrits par elle. D'un autre côté, un livre sans auteur n'a pas beaucoup de sens du tout. Je conserverais donc la propriété authors dans BookEntity mais j'introduirais la méthode suivante sur le BookRepository:

interface BookRepository extends Repository<Book, Long> {

  List<Book> findByAuthor(Author author);
}

Oui, cela nécessite que tous les clients qui auparavant pouvaient simplement appeler author.getBooks() fonctionnent désormais avec un référentiel. Mais du côté positif, vous avez supprimé toute la cruauté de vos objets de domaine et créé une direction de dépendance claire du livre à l'auteur en cours de route. Les livres dépendent des auteurs mais pas l'inverse.

39
Oliver Drotbohm

J'ai rencontré un problème similaire, lors de l'envoi de mon POJO (contenant le mappage bidirectionnel @OneToMany et @ManyToOne) en JSON via REST api, les données étaient persistantes dans les entités parent et enfant, mais le La relation de clé étrangère n'a pas été établie. Cela se produit car les associations bidirectionnelles doivent être gérées manuellement.

JPA fournit une annotation @PrePersist qui peut être utilisé pour s'assurer que la méthode annotée avec elle est exécutée avant la persistance de l'entité. Depuis, JPA insère d'abord l'entité parent dans la base de données suivie de l'entité enfant, j'ai inclus une méthode annotée avec @PrePersist qui parcourrait la liste des entités enfants et y définirait manuellement l'entité parent.

Dans votre cas, ce serait quelque chose comme ceci:

class AuthorEntitiy {
    @PrePersist
    public void populateBooks {
        for(BookEntity book : books)
            book.addToAuthorList(this);   
    }
}

class BookEntity {
    @PrePersist
    public void populateAuthors {
        for(AuthorEntity author : authors)
            author.addToBookList(this);   
    }
}

Après cela, vous pourriez obtenir une erreur de récursion infinie, pour éviter d'annoter votre classe parent avec @JsonManagedReference et votre classe enfant avec @JsonBackReference. Cette solution a fonctionné pour moi, j'espère qu'elle fonctionnera aussi pour vous.

7
Vidhi Gupta