如何与Spring Data REST和JPA保持双向关系?tl;博士问题如何解决这个问题?消除问题的根本原因

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

示例实体:

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

如果你用 一个 来支持他们,你可以得到一个 ,点击书上的链接,并用要关联的作者的URI做一个PUT。你不能走另一条路。PagingAndSortingRepositoryBookauthors

如果对作者执行 GET 并对其链接执行 PUT,则响应将返回 200,但关系永远不会持久化。books

这是预期的行为吗?


答案 1

tl;博士

关键不在于Spring Data REST中的任何内容 - 因为您可以轻松地使其在您的场景中工作 - 而是确保您的模型保持关联的两端同步。

问题

您在这里看到的问题源于Spring Data REST基本上修改了.这本身并不反映 .这必须手动解决,这不是Spring Data REST构成的约束,而是JPA的一般工作方式。您将能够通过简单地手动调用 setter 并尝试保留结果来重现错误行为。booksAuthorEntityauthorsBookEntity

如何解决这个问题?

如果删除双向关联不是一种选择(请参阅下面我为什么推荐这样做),那么实现这一目标的唯一方法是确保对关联的更改反映在两侧。通常,人们通过在添加书籍时手动将作者添加到中来解决此问题:BookEntity

class AuthorEntity {

  void add(BookEntity book) {

    this.books.add(book);

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

如果要确保从另一侧传播的更改也得到传播,还必须在一侧添加附加的 if 子句。基本上是必需的,否则这两种方法将不断调用自己。BookEntityif

默认情况下,Spring Data REST使用字段访问,因此实际上没有任何方法可以将此逻辑放入其中。一种选择是切换到属性访问并将逻辑放入 setter 中。另一种选择是使用 / 注释的方法,该方法循环访问实体并确保修改反映在两端。@PreUpdate@PrePersist

消除问题的根本原因

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

双向关联的#1规则:不要使用它们......:)

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

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

interface BookRepository extends Repository<Book, Long> {

  List<Book> findByAuthor(Author author);
}

是的,这要求以前可能只是调用的所有客户端现在都使用存储库。但从积极的一面来看,你已经从你的领域对象中删除了所有的废墟,并在此过程中创建了一个从书籍到作者的明确依赖方向。书籍取决于作者,而不是相反。author.getBooks()


答案 2

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

JPA 提供了一个注释,可用于确保在持久化实体之前执行用它注释的方法。由于 JPA 首先将父实体插入数据库,然后插入子实体,因此我添加了一个带注释的方法,该方法将循环访问子实体列表并手动将父实体设置为该方法。@PrePersist@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@JsonBackReference


推荐