如何与Spring Data REST和JPA保持双向关系?

使用Spring Data REST。 如果您具有oneToMany或ManyToOne关系,则PUT操作在“非拥有”实体上返回200,但实际上不会保留已连接的资源。

示例实体。

@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 } 

如果您使用PagingAndSortingRepository支持它们,您可以获取一本书,按照书上的作者链接进行PUT,并使用要关联的作者的URI。 你不能走另一条路。

如果您对作者进行GET并在其书籍链接上执行PUT,则响应返回200,但该关系永远不会保留。

这是预期的行为吗?

TL;博士

关键在于Spring Data REST中没有任何东西 – 因为您可以轻松地在您的场景中使其工作 – 但要确保您的模型保持关联的两端同步。

问题

你在这里看到的问题源于Spring Data REST基本上修改了AuthorEntitybooks属性。 这本身并不反映BookEntityauthors属性中的此更新。 这必须手动解决,这不是Spring Data REST组成的约束,而是JPA的一般工作方式。 您只需手动调用setter并尝试保留结果即可重现错误行为。

怎么解决这个?

如果删除双向关联不是一个选项(请参阅下面为什么我建议这样做),使这项工作的唯一方法是确保关联的更改反映在双方。 通常人们通过在添加BookEntity时手动将作者添加到BookEntity来处理此BookEntity

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

如果你想确保来自另一方的更改也被传播,那么额外的if子句也将被添加到BookEntity端。 if基本上是必需的,否则这两种方法会不断调用自己。

Spring Data REST默认使用字段访问,因此实际上没有可以将此逻辑放入的方法。 一种选择是切换到属性访问并将逻辑放入setter。 另一种选择是使用注释了@PreUpdate / @PrePersist的方法,迭代实体并确保修改反映在双方。

消除问题的根本原因

如您所见,这为域模型增加了相当多的复杂性。 我昨天在Twitter上开玩笑说:

#1双向关联规则:不要使用它们…… 🙂

如果您尽可能不尝试使用双向关系,而是回到存储库以获取构成关联背面的所有实体,它通常会简化问题。

一个很好的启发式方法来确定哪一方需要考虑关联的哪一方对于您正在建模的领域是真正的核心和关键。 在你的情况下,我认为对于一个没有她写的书而存在的作者来说完全没问题。 另一方面,没有作者的书根本没有太多意义。 所以我会在BookEntity保留authors属性,但在BookEntity引入以下方法:

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

是的,这要求之前可以调用author.getBooks()所有客户端现在可以使用存储库。 但从积极的方面来说,你已经删除了域对象中的所有内容,并在整个过程中创建了从书到作者的清晰依赖方向。 书籍取决于作者,但不是相反。

我遇到了类似的问题,在通过REST api将我的POJO(包含双向映射@OneToMany和@ManyToOne)作为JSON发送时,数据被保存在父实体和子实体中,但是没有建立外键关系。 这是因为需要手动维护双向关联。

JPA提供了一个注释@PrePersist ,可用于确保在实体持久化之前执行使用它注释的方法。 因为JPA首先将父实体插入数据库后跟子实体,所以我包含了一个用@PrePersist注释的方法,该方法将迭代子实体列表并手动设置父实体。

在你的情况下,它将是这样的:

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

在此之后,您可能会收到无限递归错误,以避免使用@JsonManagedReference注释您的父类以及使用@JsonManagedReference注释您的子类。 这个解决方案对我有用,希望它也适合你。