Java & Spring

OneToOne 관계에서 자식 엔티티를 삭제했을 경우 발생하는 DataIntegrityViolationException

ju_young 2024. 1. 3. 12:21
728x90

Error

org.springframework.dao.DataIntegrityViolationException: could not execute statement; SQL [n/a]; constraint ["FK7FUJUGL45E8HKHKIO47O2IW29: PUBLIC.ARTICLE FOREIGN KEY(ARTICLE_FILE_ID) REFERENCES PUBLIC.ARTICLE_FILE(ID) (CAST(1 AS BIGINT))"; SQL statement:
delete from article_file where id=? [23503-214]]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement

간단하게 표현하면 엔티티 간의 연관관계가 위와 같이 가지고 있는 상황이다. 그리고 ArticleFile을 삭제하려고 할 때 발생하는 에러이다.

코드

불필요한 코드는 생략하여 보기 편하게 필요한 부분만 작성했다.

1. Article

@Table(name = "article")  
@Entity  
public class Article extends AuditingFields {  
    ... (생략)

    @OneToOne(fetch = LAZY)  
    @JoinColumn(name = "article_file_id")  
    private ArticleFile articleFile;  

    ... (생략)
}

2. ArticleFile

@Table(name = "article_file")  
@Entity  
public class ArticleFile extends AuditingFields {  
    ... (생략)
    
    @OneToOne(mappedBy = "articleFile")  
	private Article article;
	
    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)  
    @JoinColumn(name = "dimension_option_id")  
    private DimensionOption dimensionOption;  
  
    ... (생략)
}

3. DimensionOption

@Table(name = "dimension_option")  
@Entity  
public class DimensionOption extends AuditingFields {  
    ... (생략) 

    @ToString.Exclude  
    @OneToMany(mappedBy = "dimensionOption", fetch = LAZY, cascade = CascadeType.ALL)  
    private final Set<Dimension> dimensions = new LinkedHashSet<>();  

    @ToString.Exclude  
    @OneToOne(mappedBy = "dimensionOption")  
    private ArticleFile articleFile;  

    ... (생략)
}

4. Dimension

@Table(name = "dimension")  
@Entity  
public class Dimension extends AuditingFields {  
    ... (생략)

    @ManyToOne(fetch = FetchType.LAZY)  
    @JoinColumn(name = "dimensionOptionId")  
    private DimensionOption dimensionOption;

    ... (생략)
}

문제

ArticleFileArticle과 연결되어있는 상황에서 ArticleFile을 삭제하게되면서 발생하는 문제이다. 정확히 말하면 참조 무결성을 해치면서 발생하는 문제이다. ArticleArticleFile의 Foreign Key를 가지고 있는데 ArticleFile을 삭제해버리면 존재하지 않는 엔티티의 ID를 참조하게 되는 것이다.

해결 방법

테스트 코드로 작성하여 설명한다.

1. 부모와의 관계를 끊고 삭제

@Test  
void deleteArticleFile() {  
    Long articleFileId = 1L;  
    long previousFileCount = articleFileRepository.count();  

    //부모와의 관계를 끊는다
    ArticleFile articleFile = articleFileRepository.getReferenceById(articleFileId);  
    Article article = articleRepository.getReferenceById(articleFile.getArticle().getId());  
    article.setArticleFile(null);

    //삭제
    articleFileRepository.deleteById(articleFileId);  

    assertThat(articleFileRepository.count()).isEqualTo(previousFileCount - 1);  
}

NOTE
Article에서 CASCADE를 적용하더라도 위처럼 부모와의 관계를 끊고 삭제한다면 자식 엔티티를 삭제할 수 있다.

2. orphanRemoval 사용

orphanRemoval는 부모와의 관계가 끊어진 자식 엔티티들을 알아서 삭제해주는 기능이다. 따라서 따로 삭제를 수행하지 않아도 부모와의 관계만 끊어주면 된다.

@Table(name = "article")  
@Entity  
public class Article extends AuditingFields {  
    ... (생략)

    @OneToOne(fetch = LAZY, orphanRemoval = true) //< 
    @JoinColumn(name = "article_file_id")  
    private ArticleFile articleFile;  

    ... (생략)
}
@Test  
void deleteArticleFile() {  
    Long articleFileId = 1L;  
    long previousFileCount = articleFileRepository.count();  

    //부모와의 관계를 끊는다
    ArticleFile articleFile = articleFileRepository.getReferenceById(articleFileId);  
    Article article = articleRepository.getReferenceById(articleFile.getArticle().getId());  
    article.setArticleFile(null);

    assertThat(articleFileRepository.count()).isEqualTo(previousFileCount - 1);  
}

3. owner 테이블 변경

owner 테이블을 ArticleFile로 변경하여 ArticleFile이 `article_id`를 외래키로 가지도록 수정하고, cascade를 설정한다.

@Table(name = "article")  
@Entity  
public class Article extends AuditingFields {  
    ... (생략)
  
    @OneToOne(mappedBy = "article", cascade = CascadeType.ALL)
    private ArticleFile articleFile;  
  
    ... (생략)
}

@Table(name = "article_file")  
@Entity  
public class ArticleFile extends AuditingFields {  
    ... (생략)
    
	@OneToOne(fetch = LAZY)
	@JoinColumn(name = "article_id")
	private Article article;
	
    @OneToOne(fetch = LAZY, cascade = CascadeType.ALL)  
	@JoinColumn(name = "dimension_option_id")
    private DimensionOption dimensionOption;  
  
    ... (생략)
}
728x90