简体   繁体   English

如何维护Spring数据REST和JPA的双向关系?

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

Working with Spring Data REST, if you have a OneToMany or ManyToOne relationship, the PUT operation returns 200 on the "non-owning" entity but does not actually persist the joined resource.使用 Spring 数据 REST,如果您有OneToManyManyToOne关系,PUT 操作会在“非拥有”实体上返回 200,但实际上不会保留已加入的资源。

Example Entities:示例实体:

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

If you back them with a PagingAndSortingRepository , you can GET a Book , follow the authors link on the book and do a PUT with the URI of a author to associate with.如果你用PagingAndSortingRepository支持它们,你可以 GET a Book ,点击书上的authors链接,并使用作者的 URI 进行 PUT 关联。 You cannot go the other way.你不能 go 反过来。

If you do a GET on an Author and do a PUT on its books link, the response returns 200, but the relationship is never persisted.如果您对作者执行 GET 操作并对其books链接执行 PUT 操作,则响应会返回 200,但这种关系永远不会持续存在。

Is this the expected behavior?这是预期的行为吗?

tl;dr TL;博士

The key to that is not so much anything in Spring Data REST - as you can easily get it to work in your scenario - but making sure that your model keeps both ends of the association in sync. 关键在于Spring Data REST中没有任何东西 - 因为您可以轻松地让它在您的场景中运行 - 但要确保您的模型保持关联的两端同步。

The problem 问题

The problem you see here arises from the fact that Spring Data REST basically modifies the books property of your AuthorEntity . 您在此处看到的问题源于Spring Data REST基本上修改了AuthorEntitybooks属性。 That itself doesn't reflect this update in the authors property of the BookEntity . 这本身并不反映BookEntityauthors属性中的此更新。 This has to be worked around manually, which is not a constraint that Spring Data REST makes up but the way that JPA works in general. 这必须手动解决,这不是Spring Data REST组成的约束,而是JPA的一般工作方式。 You will be able to reproduce the erroneous behavior by simply invoking setters manually and trying to persist the result. 您只需手动调用setter并尝试保留结果即可重现错误行为。

How to solve this? 怎么解决这个?

If removing the bi-directional association is not an option (see below on why I'd recommend this) the only way to make this work is to make sure changes to the association are reflected on both sides. 如果删除双向关联不是一个选项(请参阅下面为什么我建议这样做),使这项工作的唯一方法是确保关联的更改反映在双方。 Usually people take care of this by manually adding the author to the BookEntity when a book is added: 通常人们通过在添加BookEntity时手动将作者添加到BookEntity来处理此BookEntity

class AuthorEntity {

  void add(BookEntity book) {

    this.books.add(book);

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

The additional if clause would've to be added on the BookEntity side as well if you want to make sure that changes from the other side are propagated, too. 如果你想确保来自另一方的更改也被传播,那么额外的if子句也将被添加到BookEntity端。 The if is basically required as otherwise the two methods would constantly call themselves. if基本上是必需的,否则这两种方法会不断地自称。

Spring Data REST, by default uses field access so that theres actually no method that you can put this logic into. Spring Data REST默认使用字段访问,因此实际上没有可以将此逻辑放入的方法。 One option would be to switch to property access and put the logic into the setters. 一种选择是切换到属性访问并将逻辑放入setter。 Another option is to use a method annotated with @PreUpdate / @PrePersist that iterates over the entities and makes sure the modifications are reflected on both sides. 另一种选择是使用@PreUpdate / @PrePersist注释的方法,该方法迭代实体并确保修改反映在双方。

Removing the root cause of the issue 消除问题的根本原因

As you can see, this adds quite a lot of complexity to the domain model. 如您所见,这为域模型增加了相当多的复杂性。 As I joked on Twitter yesterday: 我昨天在Twitter上开玩笑说:

#1 rule of bi-directional associations: don't use them… :) #1双向关联规则:不要使用它们...... :)

It usually simplifies the matter if you try not to use bi-directional relationship whenever possible and rather fall back to a repository to obtain all the entities that make up the backside of the association. 如果您尽可能不尝试使用双向关系,而是回到存储库以获取构成关联背面的所有实体,它通常会简化问题。

A good heuristics to determine which side to cut is to think about which side of the association is really core and crucial to the domain you're modeling. 一个很好的启发式方法来确定哪一方需要考虑关联的哪一方对于您正在建模的领域是真正的核心和关键。 In your case I'd argue that it's perfectly fine for an author to exist with no books written by her. 在你的情况下,我认为对于一个没有她写的书而存在的作者来说完全没问题。 On the flip side, a book without an author doesn't make too much sense at all. 另一方面,没有作者的书根本没有太多意义。 So I'd keep the authors property in BookEntity but introduce the following method on the BookRepository : 所以我保持authors财产BookEntity但介绍的以下方法BookRepository

interface BookRepository extends Repository<Book, Long> {

  List<Book> findByAuthor(Author author);
}

Yes, that requires all clients that previously could just have invoked author.getBooks() to now work with a repository. 是的,这要求之前可以调用author.getBooks()所有客户端现在可以使用存储库。 But on the positive side you've removed all the cruft from your domain objects and created a clear dependency direction from book to author along the way. 但从积极的方面来说,你已经删除了域对象中的所有内容,并在整个过程中创建了从书到作者的清晰依赖方向。 Books depend on authors but not the other way round. 书籍取决于作者,但不是相反。

I faced a similar problem, while sending my POJO(containing bi-directional mapping @OneToMany and @ManyToOne) as JSON via REST api, the data was persisted in both the parent and child entities but the foreign key relation was not established. 我遇到了类似的问题,在通过REST api将我的POJO(包含双向映射@OneToMany和@ManyToOne)作为JSON发送时,数据在父实体和子实体中都保持不变,但未建立外键关系。 This happens because bidirectional associations need to be manually maintained. 这是因为需要手动维护双向关联。

JPA provides an annotation @PrePersist which can be used to make sure that the method annotated with it is executed before the entity is persisted. JPA提供了一个注释@PrePersist ,可用于确保在实体持久化之前执行使用它注释的方法。 Since, JPA first inserts the parent entity to the database followed by the child entity, I included a method annotated with @PrePersist which would iterate through the list of child entities and manually set the parent entity to it. 因为,JPA首先将父实体插入数据库后跟子实体,所以我包含了一个用@PrePersist注释的方法,该方法将迭代子实体列表并手动设置父实体。

In your case it would be something like this: 在你的情况下,它将是这样的:

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

After this you might get an infinite recursion error, to avoid that annotate your parent class with @JsonManagedReference and your child class with @JsonBackReference . 在此之后,你可能会得到一个无限递归误差,避免注释父类@JsonManagedReference和你的子类@JsonBackReference This solution worked for me, hopefully it will work for you too. 这个解决方案对我有用,希望它也适合你。

This link has a very good tutorial on how you can navigate the recursion problem: Bidirectional Relationships这个链接有一个关于如何解决递归问题的很好的教程: 双向关系

I was able to use @JsonManagedReference and @JsonBackReference and it worked like a charm我能够使用@JsonManagedReference 和@JsonBackReference,它就像一个魅力

I believe one can also utilize @RepositoryEventHandler by adding a @BeforeLinkSave handler to cross link the bidirectional relation between entities.我相信人们还可以通过添加 @BeforeLinkSave 处理程序来利用 @RepositoryEventHandler 来交叉链接实体之间的双向关系。 This seems to be working for me.这似乎对我有用。

@Component
@RepositoryEventHandler
public class BiDirectionalLinkHandler {

    @HandleBeforeLinkSave
    public void crossLink(Author author, Collection<Books> books) {
        for (Book b : books) {
            b.setAuthor(author);
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM