unidirectional relaciones relacion onetoone onetomany one manytoone many ejemplo data java spring-boot spring-data spring-data-jpa spring-data-rest

java - onetoone - relaciones hibernate annotations



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

tl; dr

La clave para eso no es tanto nada en Spring Data REST, ya que puede hacer que funcione fácilmente en su escenario, sino asegurarse 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 debe solucionarse manualmente, lo cual no es una restricción que Spring Data REST constituye sino la forma en que JPA funciona en general. Podrá reproducir el comportamiento erróneo simplemente invocando setters manualmente y tratando de persistir el resultado.

¿Cómo resolver esto?

Si eliminar la asociación bidireccional no es una opción (vea a continuación por qué recomendaría esto), la única forma de hacer que esto funcione 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 if adicional también debería agregarse en el lado BookEntity si desea asegurarse de que también se propaguen los cambios desde el otro lado. El if se requiere básicamente ya que de lo contrario los dos métodos se llamarían constantemente a sí mismos.

Spring Data REST, de manera predeterminada, utiliza el acceso de campo para que 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 establecedores. Otra opción es usar un método anotado con @PreUpdate / @PrePersist que itera sobre las entidades y se asegura de que las modificaciones se reflejen en ambos lados.

Eliminar la causa raíz del problema

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

# 1 regla de las asociaciones bidireccionales: no las use ... :)

Por lo general, simplifica el asunto si intenta no utilizar una relación bidireccional siempre que sea posible y, en cambio, recurra a un repositorio para obtener todas las entidades que conforman la parte trasera 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á modelando. En su caso, argumentaría que está perfectamente bien que exista un autor sin libros escritos por ella. Por otro lado, un libro sin autor no tiene mucho sentido. Por lo tanto, mantendría la propiedad de los authors en BookEntity pero introduciría el siguiente método en el BookRepository :

interface BookRepository extends Repository<Book, Long> { List<Book> findByAuthor(Author author); }

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

Al trabajar con Spring Data REST, si tiene una relación OneToMany o ManyToOne , la operación PUT devuelve 200 en la entidad "no propietaria" pero en realidad no persiste el recurso unido.

Entidades de ejemplo:

@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 los respalda con un PagingAndSortingRepository , puede OBTENER un Book , seguir el enlace del authors en el libro y hacer un PUT con el URI de un autor para asociarse. No puedes ir para otro lado.

Si realiza un OBTENER en un Autor y realiza un PUT en el enlace de sus books , la respuesta devuelve 200, pero la relación nunca persiste.

¿Es este el comportamiento esperado?


Me enfrenté a un problema similar, mientras enviaba mi POJO (que contiene el mapeo bidireccional @OneToMany y @ManyToOne) como JSON a través de la API REST, los datos persistieron tanto en las entidades primarias como secundarias, 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 puede usarse para asegurarse de que el método anotado con él se ejecute antes de que la entidad persista. Dado que JPA primero inserta la entidad principal en la base de datos seguida por la entidad secundaria, 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 primaria en él.

En su 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, puede obtener 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.