2017-02-02 131 views
0

使用Spring Data REST和Spring Data JPA,我想更新聚合根上的子實體集合。舉個例子來說明,假設我有一個Post實體,它與Comment實體具有一對多的關係。 Post擁有自己的Spring數據存儲庫; Comment不是因爲它只能通過Post訪問。Spring Data REST/JPA - 使用複合鍵更新OneToMany集合

令人討厭的是Comment有一個複合鍵,包括由於現有數據庫設計而導致的Post的外鍵。因此,即使我不需要雙向關係,我也找不到將外鍵作爲Comment中沒有雙向關係的組合鍵的一部分。

的類看起來像與龍目島標註以下內容:

@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> { 
} 

如果我嘗試創建一個PostComment,會發生例外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] 

回顧一下,步驟REPRO領袖是:

  1. POST沒有Comment小號
  2. 投入到創建Post一個Post一個Comment

什麼是我能做到能夠更新的最簡單方法/加Comment s到使用Spring Data REST的現有Post

演示了這方面的一個示例項目可以在這裏找到:https://github.com/shakuzen/aggregate-child-update-sample/tree/composite-key

這種特殊的設置是在倉庫中的composite-key分支。要重現上述故障與此代碼,你可以按照自述手動再現步驟或運行集成測試AggregateCompositeKeyUpdateTests.canAddCommentWithPut

回答

0

你真的不應該使用@PrePersist和​​回調管理,因爲它們的調用這些反向引用來通常取決於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); 

更新 - 春季數據REST解決方案

爲了讓Spring數據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) { 
    } 
} 

接下來你只需要確保這個bean是要麼拾起通過掃描指定各種@Component原型之一,或者您需要在配置類中將其指定爲@Bean

這兩種方法最大的區別在於第二種方法是類型安全的,實體類型由各種註釋方法的第一個參數決定。

你可以在這個here找到更多細節。

+0

感謝您的深入響應。如果我可以輕鬆地讓Spring Data REST在處理相應的HTTP請求時使用這些方法,我會很樂意按照您的建議去做,但我不確定如何最好地實現這一點。 –

+0

添加了關於如何使用偵聽器或註釋處理程序回調將我的想法與Spring Data REST集成的信息。 – Naros