¿Cómo mantener relaciones bidireccionales con Spring Data REST y JPA?

Trabajando con Spring Data REST. Si tiene una relación OneToMany o ManyToOne, la operación PUT devuelve 200 en la entidad “no propietaria” pero no persiste en realidad el recurso unido.

Ejemplo de entidades.

@Entity(name = 'author') @ToString class AuthorEntity implements Author { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) Long id String fullName @ManyToMany(mappedBy = 'authors') Set 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 authors } 

Si los respalda con un PagingAndSortingRepository , puede OBTENER un libro, seguir el enlace de los autores en el libro y hacer una PUT con el URI de un autor para asociar. No puedes ir por el otro camino.

Si haces un GET en un Autor y haces un enlace PUT en sus libros, la respuesta arroja 200, pero la relación nunca se conserva.

¿Es este el comportamiento esperado?

    tl; dr

    La clave para eso no es tanto en Spring Data REST, ya que puede hacer que funcione fácilmente en su escenario, sino asegurándose de que su modelo mantenga sincronizados ambos extremos de la asociación.

    El problema

    El problema que ve aquí surge del hecho de que Spring Data REST básicamente modifica la propiedad de books de su AuthorEntity . Eso en sí mismo no refleja esta actualización en la propiedad de los authors de BookEntity . Esto tiene que trabajarse manualmente, lo cual no es una restricción que conforma el Spring Data REST, sino la forma en que funciona el JPA en general. Podrá reproducir el comportamiento erróneo simplemente invocando setters manualmente e intentando persistir en el resultado.

    ¿Cómo resolver esto?

    Si eliminar una asociación bidireccional no es una opción (ver a continuación por qué lo recomendaría), la única forma de hacerlo funcionar es asegurarse de que los cambios en la asociación se reflejen en ambos lados. Por lo general, las personas se encargan de esto agregando manualmente el autor a BookEntity cuando se agrega un libro:

     class AuthorEntity { void add(BookEntity book) { this.books.add(book); if (!book.getAuthors().contains(this)) { book.add(this); } } } 

    La cláusula BookEntity adicional debería agregarse también en el lado de BookEntity si desea asegurarse de que los cambios del otro lado también se propaguen. El if se requiere básicamente ya que de lo contrario los dos métodos se llamarían a sí mismos constantemente.

    Spring Data REST, de forma predeterminada utiliza el acceso de campo para que en realidad no haya ningún método en el que pueda poner esta lógica. Una opción sería cambiar al acceso a la propiedad y poner la lógica en los setters. Otra opción es utilizar un método anotado con @PreUpdate / @PrePersist que itere sobre las entidades y se asegure de que las modificaciones se reflejen en ambos lados.

    Eliminando la causa raíz del problema

    Como puede ver, esto agrega bastante complejidad al modelo de dominio. Como bromeé en Twitter ayer:

    Regla # 1 de asociaciones bidireccionales: no las use … 🙂

    Por lo general, simplifica la cuestión si intenta no utilizar una relación bidireccional siempre que sea posible y, más bien, recurra a un repository para obtener todas las entidades que componen la parte posterior de la asociación.

    Una buena heurística para determinar qué lado cortar es pensar qué lado de la asociación es realmente central y crucial para el dominio que estás modelando. En su caso, argumentaría que está perfectamente bien que un autor exista sin libros escritos por ella. Por otro lado, un libro sin autor no tiene demasiado sentido. Así que mantendría la propiedad de los authors en BookEntity pero introduciré el siguiente método en el BookRepository :

     interface BookRepository extends Repository { List findByAuthor(Author author); } 

    Sí, eso requiere que todos los clientes que anteriormente podrían haber invocado author.getBooks() ahora trabajen con un repository. Pero en el lado positivo, eliminó todos los rests de los objetos de su dominio y creó una clara dirección de dependencia de libro a autor en el camino. Los libros dependen de los autores, pero no al revés.

    Me enfrenté a un problema similar, mientras enviaba mi POJO (que contenía la asignación bidireccional @OneToMany y @ManyToOne) como JSON a través de la API REST, los datos se conservaban en las entidades padre e hijo, pero no se estableció la relación de clave externa. Esto sucede porque las asociaciones bidireccionales deben mantenerse manualmente.

    JPA proporciona una anotación @PrePersist que se puede usar para asegurarse de que el método anotado se ejecuta antes de que la entidad se mantenga. Dado que, JPA primero inserta la entidad padre en la base de datos seguida de la entidad hijo, incluí un método anotado con @PrePersist que iteraría a través de la lista de entidades secundarias y establecería manualmente la entidad padre en ella.

    En tu caso, sería algo como esto:

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

    Después de esto, es posible que obtenga un error de recursión infinito, para evitar que anote su clase principal con @JsonManagedReference y su clase secundaria con @JsonBackReference . Esta solución funcionó para mí, espero que también funcione para usted.