当前位置: 首页 > 知识库问答 >
问题:

Spring Data REST/JPA-使用复合密钥更新OneToMany集合

冯星阑
2023-03-14

使用Spring数据REST和Spring数据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> {
}

如果我试图创建一个带有注释的Post,则会出现一个异常,Post_ID不能为空。换句话说,它在试图持久化的注释中缺少对父Post的反向引用。

这可以通过向维护此反向引用的Post添加@PrePer0004方法来解决:

@PrePersist
private void maintainParentBackreference() {
    for (Comment comment : this.comments) {
        comment.setPost(this);
    }
}

当创建一个新的Post时,上面的工作原理很好,但是当尝试将Comment添加到现有的Post(例如,使用PUT请求)时,它没有帮助,因为当尝试插入Comment时会出现以下错误:

NULL not allowed for column "POST_ID"; SQL statement:
insert into comment (content, id, post_id) values (?, ?, ?) [23502-193]

总而言之,复制的步骤是:

  1. 发布一篇POST而不发表评论s
  2. 在创建的帖子中添加注释

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

下面是一个示例项目,演示了这一点:https://github.com/shakuzen/aggregate-child-update-sample/tree/composite-key

此特定设置位于存储库的comical-key分支中。要使用此代码重现上述故障,您可以按照README中的手动重现步骤操作,或者运行集成测试Aggregate CompositeKeyUpdateTests.canaddCommentExput


共有1个答案

高展
2023-03-14

您确实不应该使用@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#treveComment。这些方法确保正确设置反向引用。

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直接应用此逻辑,您可以编写一个侦听器或回调类。

听众的一个例子是:

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被选中。

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

你可以在这里找到更多细节。

 类似资料:
  • 我基于以下示例实现了这一点:https://vladmihalcea.com/the-best-way-to-map-a-onetomany-association-with-jpa-and-hibernate/

  • 用户详细信息 有人能看出我做错了什么吗?

  • 问题内容: 我正在尝试使用JPA建立双向关系。我了解这是应用程序负责维护双方关系的责任。 例如,一个图书馆有多本书。在图书馆实体中,我有: 图书实体为: 不幸的是,OneToMany端的集合为空。因此,例如,对setLibrary()的调用失败,因为this.library.getBooks()。contains(this)导致NullPointerException。 这是正常行为吗?我应该自己

  • 我尝试用JPA连接两个表 第一个表与实体ReportTripSing相关联第二个表与实体TripData相关联第二个表的主键用复合键(实体TripDataPK)描述 如您所见,我想将ReportTripSingle与TripData结合起来 它不起作用:( 这是堆栈跟踪: 原因:组织。冬眠AnnotationException:com的referencedColumnNames(FTP\U ID)

  • 假设我想要一个复合键作为采购订单实体的street,city。 下面是我如何识别做这件事, 我想明白@AttributeOverrides注释到底是做什么的?即使我将colomn name更改为something STREET1,我仍然看到使用列名street创建的表。那么column=@column(name=“street”))在这里做什么。 另外,我可以将它作为PurchaseOrder类的

  • 我试图从我的模式中的数组中删除一个特定的值(一个游戏),这是代码: 模式: 错误: MongoError:E11000重复密钥错误集合:mountain。用户索引:游戏。密码_1 errmsg:'E11000重复密钥错误集合:mountain。用户索引:游戏。密码\u 1 dup密钥:{games.password:null}',[Symbol(mongoErrorContextSymbol)]: