カスケードを使用する際の注意点


持続性変換をプロジェクトに適用する際、多くの紆余曲折を経験しましたが、ここで学んだ内容をまとめたいと思います.
この投稿で使用されるエンティティのコードは次のとおりです.

Candidate Entity

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Candidate {

    @Id @GeneratedValue
    @Column(name = "candidate_id")
    private Long id;

    private int number;

    @Column(name = "candidate_name")
    private String name;

    @Column(name = "candidate_likes")
    private int likes;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "city_id")
    private City city;

    @OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL)
    private List<Sns> snsList = new ArrayList<>();

    @OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL)
    private List<Youtube> youtubeList = new ArrayList<>();

    @Builder
    public Candidate(int number, String name, City city) {
        this.number = number;
        this.name = name;
        this.likes = 0;
        this.city = city;
    }

    //== 연관관계 편의 메서드 ==//
    public void addSns(Sns sns) {
        snsList.add(sns);
        sns.setCandidate(this);
    }
}

Sns Entity

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "dtype")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Sns {

    @Id @GeneratedValue
    @Column(name = "sns_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "candidate_id")
    private Candidate candidate;

    @Column(columnDefinition = "Text")
    private String content;

    private String url;

    private LocalDateTime uploadDate;

    public Sns(String content, String url, LocalDateTime uploadDate) {
        this.content = content;
        this.url = url;
        this.uploadDate = uploadDate;
    }

    public void setCandidate(Candidate candidate) {
        this.candidate = candidate;
    }
}

Facebook Entity

@Entity
@DiscriminatorValue("F")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class Facebook extends Sns{

    private int likes;
    private int comments;
    private int shares;

    @Builder
    public Facebook(String content, String url, LocalDateTime uploadDate, int likes, int comments, int shares) {
        super(content, url, uploadDate);
        this.likes = likes;
        this.comments = comments;
        this.shares = shares;
    }

    public void change(int likes, int comments, int shares) {
        this.likes = likes;
        this.comments = comments;
        this.shares = shares;
    }
}

あなたのエンティティを実行すると、remove()も残るのではないでしょうか。


Entity Manager=em.
    @Test
    public void 영속성전이_remove() throws Exception {
        //given
        Candidate candidate = createCandidate();
        Facebook facebook = new Facebook("content1", "url", LocalDateTime.now(), 1, 1, 1);
        candidate.addSns(facebook);
        candidateRepository.save(candidate);

        //when
        facebookRepository.remove(facebook); //em.remove(facebook); 과 같은 동작입니다.

        //then
        Assertions.assertTrue(em.contains(facebook));
    }

  • 候補エンティティの@OneToMany(mappedBy="候補",cascade=CascadeType.ALL)宣言のため,候補エンティティが保持されている場合にはFacebookも構造である.

  • Entity ManagerがFacebookを削除した以上、テストは成功するでしょう?!

  • 成功した!

    でもクエリーに失敗したら...?


    上のコードにem.flush()を追加します.
        @Test
        public void 영속성전이_remove() throws Exception {
            //given
            Candidate candidate = createCandidate();
            Facebook facebook = new Facebook("content1", "url", LocalDateTime.now(), 1, 1, 1);
            candidate.addSns(facebook);
            candidateRepository.save(candidate);
    
            //when
            facebookRepository.remove(facebook);
            em.flush();
    
            //then
            Assertions.assertTrue(em.contains(facebook));
        }

    結果



    ドラゴン?!これは同じテストコードで、リフレッシュ()だけで失敗したテストになります.
    クエリーに注目する必要がありますが、insertクエリーのみ、deleteクエリーは飛んでいません.

    n/a.理由


    理由はCascadeTypeall, CascadeType.persistに設定されているためです.
    EntityManagerは、更新()とコミット()の間、永続的な管理状態にあるエンティティに対してクエリーを発行します.
  • このときEntityManagerには候補エンティティがあり、SnListにはCascadeTypeがあります.これは、すべての操作が永続的なコンテキストでFacebookに登録され、insertクエリに戻ることを意味します.
  • 解決策


    候補のsnsListからFacebookオブジェクトを削除することもできます.
        @Test
        public void 영속성전이_remove() throws Exception {
            //given
            Candidate candidate = createCandidate();
            Facebook facebook = new Facebook("content1", "url", LocalDateTime.now(), 1, 1, 1);
            candidate.addSns(facebook);
            candidateRepository.save(candidate);
    
            //when
            em.remove(facebook);
            candidate.getSnsList().remove(0);//이 부분입니다
            em.flush();
    
            //then
            Assertions.assertFalse(em.contains(facebook));
        }
  • 永続コンテキストからFacebookオブジェクトを削除します.
  • 候補のsnsListからFacebookオブジェクトを削除すると
  • クエリが
  • em.flush()から終了すると、変更+CascadeTypeが検出されます.allは、永続性コンテキストでFacebookオブジェクトを完全に切断し、deleteクエリーを実行します.
  • 解決策


    孤立Removal=true属性を候補の@OneToMany宣言に入れます.
    @Entity
    @Getter
    @NoArgsConstructor(access = AccessLevel.PROTECTED)
    public class Candidate {
    
        @Id @GeneratedValue
        @Column(name = "candidate_id")
        private Long id;
    
        private int number;
    
        @Column(name = "candidate_name")
        private String name;
    
        @Column(name = "candidate_likes")
        private int likes;
    
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "city_id")
        private City city;
    
        @OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL, orphanRemoval = true) //바뀐 부분입니다.
        private List<Sns> snsList = new ArrayList<>();
    
        @OneToMany(mappedBy = "candidate", cascade = CascadeType.ALL)
        private List<Youtube> youtubeList = new ArrayList<>();
    
        @Builder
        public Candidate(int number, String name, City city) {
            this.number = number;
            this.name = name;
            this.likes = 0;
            this.city = city;
        }
    
        //== 연관관계 편의 메서드 ==//
        public void addSns(Sns sns) {
            snsList.add(sns);
            sns.setCandidate(this);
        }
    }