如何与 Spring Data REST 和 JPA 保持双向关系?
使用 Spring Data REST,如果您有 OneToMany
或 ManyToOne
关系,则 PUT 操作在非拥有"实体上返回 200,但实际上并不持久加入的资源.
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.
示例实体:
@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
链接进行 PUT要关联的作者的 URI.你不能走另一条路.
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. You cannot go the other way.
如果您对作者执行 GET 并对其 books
链接执行 PUT,则响应返回 200,但关系永远不会持续存在.
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.
这是预期的行为吗?
推荐答案
tl;dr
关键不是 Spring Data REST 中的任何东西 - 因为您可以轻松地让它在您的场景中工作 - 但要确保您的模型保持关联的两端同步.
tl;dr
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 基本上修改了 AuthorEntity
的 books
属性.这本身并未在 BookEntity
的 authors
属性中反映此更新.这必须手动解决,这不是 Spring Data REST 构成的约束,而是 JPA 的一般工作方式.您只需手动调用设置器并尝试保持结果即可重现错误行为.
The problem you see here arises from the fact that Spring Data REST basically modifies the books
property of your AuthorEntity
. That itself doesn't reflect this update in the authors
property of the BookEntity
. 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. You will be able to reproduce the erroneous behavior by simply invoking setters manually and trying to persist the result.
如果删除双向关联不是一个选项(请参阅下文,了解我为什么推荐这样做),唯一的方法是确保对关联的更改反映在双方.通常人们通过在添加书籍时手动将作者添加到 BookEntity
来处理这一点:
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:
class AuthorEntity {
void add(BookEntity book) {
this.books.add(book);
if (!book.getAuthors().contains(this)) {
book.add(this);
}
}
}
如果您想确保来自另一端的更改也被传播,则还必须在 BookEntity
端添加额外的 if 子句.if
基本上是必需的,否则这两个方法会不断调用自己.
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. The if
is basically required as otherwise the two methods would constantly call themselves.
Spring Data REST,默认情况下使用字段访问,因此实际上没有方法可以将此逻辑放入其中.一种选择是切换到属性访问并将逻辑放入设置器中.另一种选择是使用带有 @PreUpdate
/@PrePersist
注释的方法,该方法迭代实体并确保修改反映在双方.
Spring Data REST, by default uses field access so that theres actually no method that you can put this logic into. One option would be to switch to property access and put the logic into the setters. 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.
如您所见,这给域模型增加了相当多的复杂性.正如我昨天在 Twitter 上开玩笑的那样:
As you can see, this adds quite a lot of complexity to the domain model. As I joked on Twitter yesterday:
#1 双向关联规则:不要使用它们... :)
#1 rule of bi-directional associations: don't use them… :)
如果您尽可能不使用双向关系,而是回退到存储库以获取构成关联背面的所有实体,这通常会简化问题.
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.
确定要删除哪一侧的一个很好的启发式方法是考虑关联的哪一侧对于您正在建模的领域来说是真正的核心和关键.在你的情况下,我认为一个作家没有她写的书是完全可以的.另一方面,没有作者的书根本没有太多意义.所以我会在 BookEntity
中保留 authors
属性,但在 BookRepository
上引入以下方法:
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
:
interface BookRepository extends Repository<Book, Long> {
List<Book> findByAuthor(Author author);
}
是的,这需要以前可以调用 author.getBooks()
的所有客户端现在使用存储库.但从积极的方面来说,您已经从域对象中删除了所有杂乱无章的内容,并在此过程中创建了从书籍到作者的清晰依赖方向.书籍取决于作者,但反之则不然.
Yes, that requires all clients that previously could just have invoked author.getBooks()
to now work with a repository. 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.
相关文章