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

travail avec le repos de données de printemps. 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 soutenez avec un PagingAndSortingRepository, Vous pouvez obtenir un livre, suivre le lien auteurs 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 faites un GET sur un auteur et faire un PUT sur son lien livres, la réponse renvoie 200, mais la relation n'est jamais persistante.

c'Est le comportement attendu?

21
demandé sur Oliver Gierke 2015-05-26 20:05:44

2 réponses

tl;dr

La clé qui n'est pas tellement quelque chose au Printemps de Données RESTE - comme vous pouvez facilement le faire fonctionner dans votre scénario - mais assurez-vous que votre modèle garde les deux extrémités de l'association dans la synchronisation.

Le problème

le problème que vous voyez ici provient du fait que le repos des données de printemps modifie fondamentalement le books propriété de votre AuthorEntity. Cela ne reflète pas cette mise à jour dans le authors propriété de l' BookEntity. Ce doit être fonctionnait manuellement, ce qui n'est pas une contrainte que le repos des données de printemps constitue, mais la façon dont JPA fonctionne en général. Vous serez en mesure de reproduire le comportement erroné en invoquant simplement les setters manuellement et en essayant de persister le résultat.

comment résoudre cela?

si supprimer l'association bidirectionnelle n'est pas une option (Voir ci-dessous pourquoi je recommande cela) la seule façon de faire ce travail est de s'assurer que les changements à l'association sont reflétés sur les deux côté. Habituellement les gens prennent soin de cela en ajoutant manuellement l'auteur à la BookEntity quand un livre est ajouté:

class AuthorEntity {

  void add(BookEntity book) {

    this.books.add(book);

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

la clause additional if devrait être ajoutée sur le BookEntity côté aussi si vous voulez vous assurer que les changements de l'autre côté se propagent aussi. if est fondamentalement nécessaire car autrement les deux méthodes s'appelleraient constamment.

le repos de données de ressort, par défaut utilise l'accès de champ de sorte qu'il n'y a en fait aucune méthode que 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 réfléchies des deux côtés.

Suppression de la cause racine 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 de associations bidirectionnelles: ne les utilisez pas...:)

il simplifie habituellement la question si vous essayez de ne pas utiliser la relation bidirectionnelle dans la mesure du possible et de vous rabattre sur un dépôt pour obtenir toutes les entités qui constituent le revers de l'association.

une bonne heuristique pour déterminer quel côté couper est de penser à quel côté de l'association est vraiment le cœur et crucial pour le domaine que vous modélisez. Dans votre cas, je dirais que c'est parfaitement bien pour un auteur d'exister sans livres écrits par elle. D'un autre côté, un livre sans auteur n'a pas beaucoup de sens. J'aimerais garder l' authors propriété BookEntity mais introduisez la méthode suivante sur le BookRepository:

interface BookRepository extends Repository<Book, Long> {

  List<Book> findByAuthor(Author author);
}

Oui, qui exige que tous les clients qui ont précédemment pourrait juste avoir invoqué author.getBooks() maintenant travailler avec un référentiel. Mais d'un point de vue positif, vous avez supprimé tous les crus de vos objets de domaine et créé une dépendance claire. direction du livre à l'auteur le long du chemin. Livres dépendent des auteurs, mais pas l'inverse.

32
répondu Oliver Gierke 2015-05-27 06:02:24

j'ai fait face à un problème similaire, en envoyant mon POJO(contenant la cartographie bidirectionnelle @OneToMany et @ManyToOne) comme JSON via l'api REST, les données ont persisté dans les deux entités parent et enfant mais la relation clé étrangère n'a pas été établie. Cela se produit parce que les associations bidirectionnelles doivent être maintenues 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 que l'entité n'est conservée. Depuis, JPA insère d'abord l'entité mère dans la base de données suivie de l'entité enfant, j'ai inclus une méthode annotée avec @PrePersist qui permettrait de parcourir la liste des enfants et de définir manuellement l'entité mère.

Dans votre cas, il 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 avoir une erreur de récursion infinie, pour éviter que annoter votre classe de parent avec @JsonManagedReference et votre classe d'enfant avec @JsonBackReference. Cette solution a fonctionné pour moi, j'espère qu'il va travailler pour vous aussi.

3
répondu Vidhi Gupta 2017-10-27 10:04:39