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

kea*_*gik 27 java spring-data spring-data-jpa spring-data-rest spring-boot

使用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<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
}
Run Code Online (Sandbox Code Playgroud)

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

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

这是预期的行为吗?

Oli*_*ohm 41

TL;博士

关键在于Spring Data REST中没有任何东西 - 因为您可以轻松地让它在您的场景中运行 - 但要确保您的模型保持关联的两端同步.

问题

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

怎么解决这个?

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

class AuthorEntity {

  void add(BookEntity book) {

    this.books.add(book);

    if (!book.getAuthors().contains(this)) {
       book.add(this);
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

BookEntity如果你想确保来自另一方的更改也会传播,那么附加的if子句也会被添加到侧面.该if否则这两种方法将不断称自己基本上是必需的.

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

消除问题的根本原因

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

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

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

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

interface BookRepository extends Repository<Book, Long> {

  List<Book> findByAuthor(Author author);
}
Run Code Online (Sandbox Code Playgroud)

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

  • 你的答案的"模板"应该以某种方式标准化为SO:P!好东西! (5认同)
  • 只是为了添加对JPA规范的引用,在"JSR-000317 Java(tm)Persistence 2.0"规范的PDF版本的第42页(第2.9章)中指出:"请注意,应用程序负责保持运行时关系的一致性 - 例如,当应用程序在运行时更新关系时,确保双向关系的"一"和"多"方彼此一致. (3认同)
  • 如果我读得很好,您是建议删除所有 OneToMany 关系并仅保留它们的 ManyToOne 挂件?这将是我建模实体的方式的真正重大变化...... (2认同)
  • @OliverGierke 有趣的回复。我并不完全相信双向关联。让我们考虑一个具有 List&lt;Payment&gt; 的票证。我们可以争辩说,没有 Payment 的票也可以存在,相反,没有相关票的 Payment 没有多大意义。因此,我会将票证属性保留在 Payment 中。但是通过这种方式,您如何创建 bean 验证规则,例如简单的“付款总额不应 &gt; 票价”?你会把它放在付款上吗?这是一个好习惯吗?谢谢 (2认同)

小智 7

我遇到类似的问题,同时通过REST API将我的POJO(包含双向映射@OneToMany和@ManyToOne)作为JSON发送为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);   
    }
}
Run Code Online (Sandbox Code Playgroud)

此后,您可能会遇到无限递归错误,以避免用注释您的父类@JsonManagedReference和您的子类@JsonBackReference。该解决方案对我有用,希望它也对您有用。