Spring 数据 REST/JPA - 使用复合键更新 OneToMany 集合
Spring Data REST/JPA - Update OneToMany collection with composite key
使用 Spring Data REST 和 Spring Data JPA,我想更新聚合根上的子实体集合。作为演示的示例,假设我有一个 Post
实体,它与 Comment
实体具有一对多关系。 Post
拥有自己的 Spring 数据存储库; Comment
没有,因为它只能通过 Post
.
访问
由于现有的数据库设计,Comment
有一个复合键,其中包括指向 Post
的外键。因此,即使我不需要双向关系,我也找不到使外键成为 Comment
中复合键的一部分的方法,即使我不需要双向关系。
带有 Lombok 注释的 类 如下所示:
@Entity
@Data
public class Post {
@Id
@GeneratedValue
private long id;
@OneToMany(mappedBy = "post", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Comment> comments = new HashSet<>();
private String title;
}
以及评论:
@Entity
@IdClass(Comment.CommentPk.class)
@Data
@EqualsAndHashCode(exclude = "post")
@ToString(exclude = "post")
public class Comment {
@Id
private long id;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@RestResource(exported = false)
@JsonIgnore
private Post post;
private String content;
@Data
static class CommentPk implements Serializable {
private long id;
private Post post;
}
}
存储库:
public interface PostRepository extends JpaRepository<Post, Long> {
}
如果我尝试用 Comment
创建一个 Post
,将出现 POST_ID
不能为 NULL 的异常。换句话说,它在它试图保留的 Comment
中缺少对父 Post
的反向引用。
这可以通过将 @PrePersist
方法添加到维护此反向引用的 Post
来解决:
@PrePersist
private void maintainParentBackreference() {
for (Comment comment : this.comments) {
comment.setPost(this);
}
}
以上在创建新的 Post
时工作正常,但在尝试将 Comment
添加到现有 Post
时没有帮助(例如使用 PUT 请求),因为尝试插入评论时会出现以下错误:
NULL not allowed for column "POST_ID"; SQL statement:
insert into comment (content, id, post_id) values (?, ?, ?) [23502-193]
回顾一下,重现的步骤是:
- POST 一个
Post
没有 Comment
s
- PUT 到创建的
Post
with a Comment
我可以使用 Spring 数据 REST update/add Comment
到现有 Post
的最简单方法是什么?
可在此处找到演示这一点的示例项目:https://github.com/shakuzen/aggregate-child-update-sample/tree/composite-key
此特定设置位于存储库的 composite-key
分支中。要使用此代码重现上述故障,您可以按照 README 中的手动重现步骤或 运行 集成测试 AggregateCompositeKeyUpdateTests.canAddCommentWithPut
你真的不应该使用 @PrePersist
和 @PreUpdate
回调来管理这些 back-references 因为它们的调用通常取决于 Post
的状态是否实际上是是否被操纵
相反,这些关系应该是您作为控制器或某些业务服务调用的某些 domain-specific 代码的一部分进行操作的东西。我通常更喜欢将这些类型的关系抽象为更领域驱动的设计方法:
public class Post {
@OneToMany(mappedBy = "Post", cascade = CascadeType.ALL, ...)
private Set<Comment> comments;
// Allows access to the getter, but it protects the internal collection
// from manipulation from the outside, forcing users to use the DDD methods.
public Set<Comment> getComments() {
return Collections.unmodifiableSet( comments );
}
// Do not expose the setter because we want to control adding/removing
// of comments through a more DDD style.
private void setComments(Set<Comment> comments) {
this.comments = comments;
}
public void addComment(Comment comment) {
if ( this.comments == null ) {
this.comments = new HashSet<Comment>();
}
this.comments.add( comment );
comment.setPost( this );
}
public void removeComment(Comment comment) {
if ( this.comments != null ) {
for ( Iterator<Comment> i = comments.iterator(); i.hasNext(); ) {
final Comment postComment = i.next();
if ( postComment.equals( comment ) ) {
// uses #getCompositeId() equality
iterator.remove();
comment.setPost( null );
return;
}
}
}
throw new InvalidArgumentException( "Comment not associated with post" );
}
正如您从代码中看到的,Post
实体对象的用户如果希望操纵关联的注释,则必须使用 #addComment
和 #removeComment
。这些方法确保 back-reference 设置正确。
final Comment comment = new Comment();
// set values on comment
final Post post = entityManager.find( Post.class, postId );
post.addComment( comment );
entityManager.merge( post );
更新 - Spring 数据 REST 解决方案
为了让 Spring Data REST 直接应用此逻辑,您可以编写侦听器或回调 class。
监听器的一个例子是:
public class BeforeSavePostEventListener extends AbstractRepositoryEventListener {
@Override
public void onBeforeSave(Object entity) {
// logic to do by inspecting entity before repository saves it.
}
}
带注释的处理程序的示例是:
@RepositoryEventHandler
public class PostEventHandler {
@HandleBeforeSave
public void handlePostSave(Post p) {
}
@HandleBeforeSave
public void handleCommentSave(Comment c) {
}
}
接下来,您只需要通过扫描指定各种 @Component
构造型之一来确保拾取此 bean,或者您需要在配置中将其指定为 @Bean
class.
这两种方法最大的区别是第二种方法是 type-safe 而 entity-type 是由各种注释方法的第一个参数决定的。
您可以在此找到更多详细信息 here。
使用 Spring Data REST 和 Spring Data JPA,我想更新聚合根上的子实体集合。作为演示的示例,假设我有一个 Post
实体,它与 Comment
实体具有一对多关系。 Post
拥有自己的 Spring 数据存储库; Comment
没有,因为它只能通过 Post
.
由于现有的数据库设计,Comment
有一个复合键,其中包括指向 Post
的外键。因此,即使我不需要双向关系,我也找不到使外键成为 Comment
中复合键的一部分的方法,即使我不需要双向关系。
带有 Lombok 注释的 类 如下所示:
@Entity
@Data
public class Post {
@Id
@GeneratedValue
private long id;
@OneToMany(mappedBy = "post", fetch = FetchType.EAGER, cascade = CascadeType.ALL, orphanRemoval = true)
private Set<Comment> comments = new HashSet<>();
private String title;
}
以及评论:
@Entity
@IdClass(Comment.CommentPk.class)
@Data
@EqualsAndHashCode(exclude = "post")
@ToString(exclude = "post")
public class Comment {
@Id
private long id;
@Id
@ManyToOne(fetch = FetchType.LAZY)
@RestResource(exported = false)
@JsonIgnore
private Post post;
private String content;
@Data
static class CommentPk implements Serializable {
private long id;
private Post post;
}
}
存储库:
public interface PostRepository extends JpaRepository<Post, Long> {
}
如果我尝试用 Comment
创建一个 Post
,将出现 POST_ID
不能为 NULL 的异常。换句话说,它在它试图保留的 Comment
中缺少对父 Post
的反向引用。
这可以通过将 @PrePersist
方法添加到维护此反向引用的 Post
来解决:
@PrePersist
private void maintainParentBackreference() {
for (Comment comment : this.comments) {
comment.setPost(this);
}
}
以上在创建新的 Post
时工作正常,但在尝试将 Comment
添加到现有 Post
时没有帮助(例如使用 PUT 请求),因为尝试插入评论时会出现以下错误:
NULL not allowed for column "POST_ID"; SQL statement:
insert into comment (content, id, post_id) values (?, ?, ?) [23502-193]
回顾一下,重现的步骤是:
- POST 一个
Post
没有Comment
s - PUT 到创建的
Post
with aComment
我可以使用 Spring 数据 REST update/add Comment
到现有 Post
的最简单方法是什么?
可在此处找到演示这一点的示例项目:https://github.com/shakuzen/aggregate-child-update-sample/tree/composite-key
此特定设置位于存储库的 composite-key
分支中。要使用此代码重现上述故障,您可以按照 README 中的手动重现步骤或 运行 集成测试 AggregateCompositeKeyUpdateTests.canAddCommentWithPut
你真的不应该使用 @PrePersist
和 @PreUpdate
回调来管理这些 back-references 因为它们的调用通常取决于 Post
的状态是否实际上是是否被操纵
相反,这些关系应该是您作为控制器或某些业务服务调用的某些 domain-specific 代码的一部分进行操作的东西。我通常更喜欢将这些类型的关系抽象为更领域驱动的设计方法:
public class Post {
@OneToMany(mappedBy = "Post", cascade = CascadeType.ALL, ...)
private Set<Comment> comments;
// Allows access to the getter, but it protects the internal collection
// from manipulation from the outside, forcing users to use the DDD methods.
public Set<Comment> getComments() {
return Collections.unmodifiableSet( comments );
}
// Do not expose the setter because we want to control adding/removing
// of comments through a more DDD style.
private void setComments(Set<Comment> comments) {
this.comments = comments;
}
public void addComment(Comment comment) {
if ( this.comments == null ) {
this.comments = new HashSet<Comment>();
}
this.comments.add( comment );
comment.setPost( this );
}
public void removeComment(Comment comment) {
if ( this.comments != null ) {
for ( Iterator<Comment> i = comments.iterator(); i.hasNext(); ) {
final Comment postComment = i.next();
if ( postComment.equals( comment ) ) {
// uses #getCompositeId() equality
iterator.remove();
comment.setPost( null );
return;
}
}
}
throw new InvalidArgumentException( "Comment not associated with post" );
}
正如您从代码中看到的,Post
实体对象的用户如果希望操纵关联的注释,则必须使用 #addComment
和 #removeComment
。这些方法确保 back-reference 设置正确。
final Comment comment = new Comment();
// set values on comment
final Post post = entityManager.find( Post.class, postId );
post.addComment( comment );
entityManager.merge( post );
更新 - Spring 数据 REST 解决方案
为了让 Spring Data REST 直接应用此逻辑,您可以编写侦听器或回调 class。
监听器的一个例子是:
public class BeforeSavePostEventListener extends AbstractRepositoryEventListener {
@Override
public void onBeforeSave(Object entity) {
// logic to do by inspecting entity before repository saves it.
}
}
带注释的处理程序的示例是:
@RepositoryEventHandler
public class PostEventHandler {
@HandleBeforeSave
public void handlePostSave(Post p) {
}
@HandleBeforeSave
public void handleCommentSave(Comment c) {
}
}
接下来,您只需要通过扫描指定各种 @Component
构造型之一来确保拾取此 bean,或者您需要在配置中将其指定为 @Bean
class.
这两种方法最大的区别是第二种方法是 type-safe 而 entity-type 是由各种注释方法的第一个参数决定的。
您可以在此找到更多详细信息 here。