Spring Data REST / JPA - 使用组合键更新OneToMany集合

时间:2017-02-02 10:32:38

标签: hibernate jpa spring-data-jpa spring-data-rest

使用Spring Data REST和Spring Data JPA,我想在聚合根上更新子实体集合。作为演示的示例,我们假设我有一个与Post实体具有一对多关系的Comment实体。 Post有自己的Spring Data存储库; 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> {
}

如果我尝试使用Post创建Comment,则会发生POST_ID不能为NULL的异常。换句话说,它缺少对它试图保留的Post中的父Comment的反向引用。

可以通过向@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]

总结一下,重现的步骤是:

  1. 发布Post,但没有Comment s
  2. PUT到创建的Post a Comment
  3. 使用Spring Data REST能够更新/添加Comment到现有Post的最简单方法是什么?

    可以在此处找到演示此内容的示例项目:https://github.com/shakuzen/aggregate-child-update-sample/tree/composite-key

    此特定设置位于存储库的composite-key分支中。要使用此代码重现上述故障,您可以按照自述文件中的手动复制步骤进行操作或运行集成测试AggregateCompositeKeyUpdateTests.canAddCommentWithPut

1 个答案:

答案 0 :(得分:0)

你真的不应该使用@PrePersist@PreUpdate回调来管理这些反向引用,因为它们的调用通常取决于Post的状态是否实际被操纵

相反,这些关系应该是您作为控制器或某些业务服务调用的某些特定于域的代码的一部分进行操作的内容。我更喜欢在更多领域驱动设计方法背后提取这些类型的关系:

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。这些方法可确保正确设置反向引用。

final Comment comment = new Comment();
// set values on comment
final Post post = entityManager.find( Post.class, postId );
post.addComment( comment );
entityManager.merge( post );

更新 - Spring Data REST解决方案

为了让Spring Data REST直接应用这个逻辑,你可以编写一个监听器或一个回调类。

听众的一个例子是:

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。< / p>

这两种方法的最大区别在于第二种方法是类型安全的,而实体类型是由各种带注释方法的第一个参数决定的。

您可以在here找到更多详细信息。