如何维护与 Spring Data REST 和 JPA 的双向关系?

How to maintain bi-directional relationships with Spring Data REST and JPA?

使用 Spring 数据 REST,如果您有 OneToManyManyToOne 关系,"non-owning" 实体上的 PUT 操作 returns 200 但是实际上并没有保留加入的资源。

示例实体:

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

如果你用 PagingAndSortingRepository 支持它们,你可以得到一个 Book,按照书上的 authors link 然后用 URI 做一个 PUT与之交往的作者。你不能走另一条路。

如果您对作者执行 GET 并对其 books link 执行 PUT,则响应 returns 200,但这种关系永远不会持续存在。

这是预期的行为吗?

tl;博士

关键不是 Spring Data REST 中的任何东西——因为您可以轻松地让它在您的场景中工作——但要确保您的模型使关联的两端保持同步。

问题

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

如何解决这个问题?

如果删除双向关联不是一个选项(请参阅下文了解我推荐此操作的原因),使这项工作有效的唯一方法是确保对关联的更改反映在双方。通常人们通过在添加一本书时手动将作者添加到 BookEntity 来解决这个问题:

class AuthorEntity {

  void add(BookEntity book) {

    this.books.add(book);

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

如果您希望确保来自另一端的更改也被传播,则还必须在 BookEntity 端添加额外的 if 子句。 if 基本上是必需的,否则这两种方法会不断地调用自己。

Spring 数据 REST,默认情况下使用字段访问,因此实际上没有可以将此逻辑放入其中的方法。一种选择是切换到 属性 访问并将逻辑放入设置器中。另一种选择是使用带有 @PreUpdate/@PrePersist 注释的方法迭代实体并确保修改反映在两侧。

消除问题的根本原因

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

#1 rule of bi-directional associations: don't use them… :)

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

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

interface BookRepository extends Repository<Book, Long> {

  List<Book> findByAuthor(Author author);
}

是的,这要求以前只能调用 author.getBooks() 的所有客户端现在都可以使用存储库。但从积极的方面来说,您已经从域对象中删除了所有的杂物,并在整个过程中创建了从书籍到作者的明确依赖方向。书靠作者,反之则不然。

我遇到了类似的问题,在通过 REST api 将我的 POJO(包含 bi-directional 映射 @OneToMany 和 @ManyToOne)发送为 JSON 时,数据在两个parent 和 child 个实体,但未建立外键关系。发生这种情况是因为需要手动维护双向关联。

JPA 提供了注解@PrePersist,可用于确保在实体持久化之前执行用它注解的方法。由于 JPA 首先将 parent 实体插入数据库,然后是 child 实体,因此我包含了一个用 @PrePersist 注释的方法,它将遍历 child 实体的列表,并且手动为其设置 parent 实体。

在你的情况下它会是这样的:

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

在此之后你可能会遇到无限递归错误,为了避免这种情况,请将你的 parent class 注释为 @JsonManagedReference 并将你的 child class 注释为 @JsonBackReference。这个解决方案对我有用,希望它对你也有用。

这个 link 有一个关于如何解决递归问题的非常好的教程:Bidirectional Relationships

我能够使用@JsonManagedReference 和@JsonBackReference,而且效果很好