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

Posted

技术标签:

【中文标题】如何与 Spring Data REST 和 JPA 保持双向关系?【英文标题】:How to maintain bi-directional relationships with Spring Data REST and JPA? 【发布时间】:2015-08-08 11:53:16 【问题描述】:

使用 Spring Data REST,如果您有 OneToManyManyToOne 关系,则 PUT 操作会在“非拥有”实体上返回 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 链接并使用要关联的作者的URI 执行PUT。你不能走另一条路。

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

这是预期的行为吗?

【问题讨论】:

双向关联必须在对象模型中手动维护,这通常在设置器中完成。 Spring Data REST 默认使用字段访问。这意味着根据定义,关联的变化不会反映在关联的另一端。您是否尝试过在@PrePersist/@PreUpdate 方法中触发同步?还是切换到属性访问? 我也会投票解决双向关联问题。 Author 可以在没有Book 的情况下存在。可以使用存储库方法Set&lt;Book&gt; BookRepository.findByAuthor(Author author) 检索Books 的Author。这大大简化了模型,因为您不必手动维护引用。 我可以看到一个图书馆界面,您需要在其中找到作者,然后查看它的书籍。如果没有双向关联,这将不受支持。如果不是 DATA REST 的意图,则不喜欢管理实体关联。感谢您的输入,过滤作者的书籍似乎是最好的方法。尽可能不要让事情单向。 :) 【参考方案1】:

这个链接有一个很好的教程,告诉你如何解决递归问题:Bidirectional Relationships

我能够使用@JsonManagedReference 和@JsonBackReference,它就像一个魅力

【讨论】:

【参考方案2】:

我遇到了类似的问题,在通过 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 注释您的父类和使用@JsonBackReference 注释您的子类。这个解决方案对我有用,希望它也对你有用。

【讨论】:

面临同样的问题,但@PrePersist 对我不起作用,因为关系字段在@PrePersist 回调中返回null,并且其中没有设置子/父。【参考方案3】:

tl;博士

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

问题

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

如何解决?

如果删除双向关联不是一个选项(请参阅下文,了解我为什么推荐这样做),唯一的方法是确保对关联的更改反映在双方。通常人们会在添加书籍时手动将作者添加到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,默认情况下使用字段访问,因此实际上没有方法可以将此逻辑放入其中。一种选择是切换到属性访问并将逻辑放入设置器中。另一种选择是使用带有 @PreUpdate/@PrePersist 注释的方法,该方法迭代实体并确保修改反映在双方。

消除问题的根本原因

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

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

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

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

interface BookRepository extends Repository<Book, Long> 

  List<Book> findByAuthor(Author author);

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

【讨论】:

您的答案的“模板”应该以某种方式在 SO :P 上标准化!好东西! 如果我没看错,您是在建议删除所有 OneToMany 关系并只保留它们的 ManyToOne 挂件?这将是我为实体建模的方式的一个真正的重大变化...... 没有。我不推荐任何基于注释的东西。我建议不要使用双向关联。事实上,我什至没有提到任何注释,所以对我来说,你如何得出这种解释并不完全清楚。您切断关联的哪一边在很大程度上取决于用例。请重新阅读提及启发式的部分以进行论证。 只是为了添加对 JPA 规范的引用,在“JSR-000317 Java(tm) Persistence 2.0”规范的 PDF 版本的第 42 页(第 2.9 章)中声明:“请注意,应用程序负责维护运行时关系的一致性——例如,当应用程序在运行时更新关系时,确保双向关系的“单”和“多”端彼此一致。” @OliverGierke 有趣的回复。我并不完全相信双向关联。让我们考虑一下具有 List 的 Ticket。我们可以争辩说一张票也可以在没有付款的情况下存在,相反,没有相关票的付款没有多大意义。因此,我会将票证属性保留在 Payment 中。但是通过这种方式,您如何创建 bean 验证规则,例如简单的“付款总和不应 > 票价”?你会把它放在付款上吗?这是一个好习惯吗?谢谢

以上是关于如何与 Spring Data REST 和 JPA 保持双向关系?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 REST 端点中发布具有关系的实体 - Kotlin Spring Data

如何使用 @EmbeddedId 使用 Spring Data REST 和 ConversionService?

Spring HATEOAS 与 Spring Data Rest

如何在 Spring-Data-Rest 中实现细粒度的访问控制?

如何使用 Querydsl 和 Spring Data 轻松实现“REST API 查询语言”来过滤实体?

如何在 Spring Boot 中使用带有 Bearer Token 和 form-data 的 Rest Template 调用 REST Api