【问题标题】:Spring Data REST/JPA - Update OneToMany collection with composite keySpring Data REST/JPA - 使用复合键更新 OneToMany 集合
【发布时间】:2017-02-02 10:32:37
【问题描述】:

使用 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> {
}

如果我尝试使用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]

回顾一下,重现的步骤是:

  1. 发布一个Post 没有Comments
  2. PUT 到创建的Post with Comment

使用 Spring Data REST 将Comments 更新/添加到现有的Post 时,最简单的方法是什么?

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

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

【问题讨论】:

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


    【解决方案1】:

    您真的不应该使用@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 实体对象的用户如果希望操作关联的 cmets,则必须使用 #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

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

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

    【讨论】:

    • 感谢您的深入回复。如果我可以轻松地让 Spring Data REST 在处理相应的 HTTP 请求时使用这些方法,我会很乐意按照您的建议进行操作,但我不确定如何最好地实现这一点。
    • 添加了有关如何使用侦听器或带注释的处理程序回调将我的想法与 Spring Data REST 集成的信息。
    • 但是仅仅使用 PrePersist/PreUpdate 注释看起来不是更干净吗,因为它涵盖了两种情况(Spring Data Rest 和调用添加/删除方法)?
    • @EricTurley 这值得商榷。我发现在即将调用save/updatepersist/merge 的业务代码中分配关联更简洁,而不是解耦并在某些回调期间调用它。简而言之,如果我可以分配其他值,那么我没有理由不能完全为关联执行此操作,以便在调用SessionEntityManager 之前,实体状态已完成。当您需要使用无状态会话并且不再调用这些回调时会发生什么?最简洁的解决方案是简单地将实体填充在一起。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-26
    • 2017-06-05
    • 2017-05-29
    • 2015-01-24
    • 1970-01-01
    相关资源
    最近更新 更多