Published 2023. 1. 15. 22:20
반응형
문제 상황
- Jpa로 값을 삭제하려 했지만 SQLIntegrityConstraintViolationException 에러가 발생
- DB에서 확인해봐도 값이 정상적으로 삭제되지 않았음
원인
- 연관관계 매핑이 되어있는 상황에서는 부모 객체 삭제 시 자식 객체에서 부모 객체를 참조하고 있으면 에러가 발생하고 값이 삭제되지 않음
- Parent와 Child가 1:N 관계를 맺고 있는 상황이고, Child가 연관관계 주인이라면 Child는 쉽게 삭제가 가능하지만, Parent를 삭제하려면 따로 처리가 필요함
- [Spring Boot] 연관관계 매핑 참고
해결 방법
- Cascade 사용
- OrphanRemoval 사용
- Soft Delete(논리 삭제) 방식 사용
- 각각의 방법을 예제를 통해 정리
예제 설명
- Child, Parent, GrandParent 객체 존재
- Child는 Parent를 한 명만 가질 수 있지만, Parent는 Child를 여러 명 가질 수 있음
- Child : Parent = 1 : N 관계
- Parent는 GrandParent를 한 명만 가질 수 있지만, GrandParent는 Parent를 여러 명 가질 수 있음
- Parent : GrandParent = 1 : N 관계
@Entity
public class Child {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Parent parent;
}
@Entity
public class Parent {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@OneToMany(mappedBy = "parent")
List<Child> children = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "grand_parent_id")
private GrandParent grandParent;
}
@Entity
public class GrandParent {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@OneToMany(mappedBy = "grandParent")
List<Parent> parents = new ArrayList<>();
}
Cascade (영속성 전이)
Cascade란?
- 부모 Entity를 영속 상태로 만들 때 자식 Entity도 함께 영속 상태로 만들고 싶은 경우에 사용
- ex) 부모 Entity 삭제 시 자식 Entity도 삭제
- @ManyToOne, @OneToMany와 같은 어노테이션에 사용 가능
Cascade 종류
- ALL : 모든 Cascade 적용
- PERSIST : Entity를 영속화 할 때, 연관된 Entity도 함께 영속화
- REMOVE : Entity를 제거할 때, 연관된 Entity도 삭제
- MERGE : Entity를 병합할 때, 연관된 Entity도 병합
- REFRESH : Entity를 새로고침 할 때, 연관된 Entity도 새로고침
- DETACH : Entity를 detach 할 때, 연관된 Entity도 detach
CascadeType.REMOVE 사용 방법
- 위 예제에서는 GrandParent, Parent가 부모 객체
- 아래와 같이 GrandParent-Parent의 관계, Parent-Child의 관계에서 부모쪽에 CascadeType.REMOVE를 적용하면 부모 객체도 삭제가 가능해지고, 부모 객체 삭제 시 모든 자식 객체 삭제
- 만약 Parent-Child의 관계에만 적용한다면 Parent는 삭제가 가능하지만, Grand-Parent를 삭제하면 에러 발생
- 만약 GrandParent-Parent의 관계에만 적용한다면 GrandParent, Parent 삭제 시 에러 발생
- Parent가 삭제되는 경우에 에러가 발생하기 때문에, GrandParent도 삭제할 수 없음
@Entity
public class GrandParent {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@OneToMany(mappedBy = "grandParent", cascade = CascadeType.REMOVE)
List<Parent> parents = new ArrayList<>();
}
@Entity
public class Parent {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
List<Child> children = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "grand_parent_id")
private GrandParent grandParent;
}
OrphanRemoval
- orpahnRemoval이란 고아 객체(Orphan)을 제거한다는 뜻으로, 부모 Entity와의 연관관계가 끊어진 자식 Entity를 자동으로 삭제하는 기능
orphanRemoval 사용 방법
- cascade와 마찬가지로 GrandParent와 Parent Class를 아래와 같이 수정하면 삭제가 정상적으로 가능해짐
- 만약 Parent-Child의 관계에만 적용한다면 Parent는 삭제가 가능하지만, Grand-Parent를 삭제하면 에러 발생
- 만약 GrandParent-Parent의 관계에만 적용한다면 GrandParent, Parent 삭제 시 에러 발생
- Parent가 삭제되는 경우에 에러가 발생하기 때문에, GrandParent도 삭제할 수 없음
@Entity
public class GrandParent {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@OneToMany(mappedBy = "grandParent", cascade = CascadeType.REMOVE)
List<Parent> parents = new ArrayList<>();
}
@Entity
public class Parent {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
List<Child> children = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "grand_parent_id")
private GrandParent grandParent;
}
CascadeType.REMOVE, OrphanRemoval 차이
- OrphanRemoval은 부모-자식 관계가 끊어지면 자식을 삭제
- CascadeType.REMOVE는 부모-자식 관계가 끊어져도 자식을 삭제하지 않음
- 이를 테스트해보기 위해서 CascadeType.REMOVE, OrphanRemoval과 CascadeType.PERSIST를 같이 사용해서 테스트 진행
- 자식을 삭제하지 않고 아래와 같이 부모-자식 관계만 제거
@DeleteMapping("/child/{childId}")
public String deleteChild(@PathVariable Long childId) {
Child child = childRepository.findById(childId).get();
Parent parent = child.getParent();
parent.getChildren().remove(child);
child.setParent(null);
childRepository.save(child);
parentRepository.save(parent);
return childId + "번 자식 삭제 완료";
}
- Parent Entity에서 아래와 같이 orphanRemoval을 적용한 경우에는 자식이 삭제됨
@OneToMany(mappedBy = "parent", orphanRemoval = true, cascade = CascadeType.PERSIST)
List<Child> children = new ArrayList<>();
- 하지만 Parent Entity에서 아래와 같이 CascadeType.REMOVE를 적용한 경우에는 자식이 삭제되지 않음
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
List<Child> children = new ArrayList<>();
Soft Delete
- 위에서 설명한 방법들은 부모 객체를 삭제하면 자식 객체들을 삭제함
- Soft Delete(논리 삭제)를 하면 부모 객체 삭제 시 부모 객체가 실제로는 지워지지 않고, 지워졌다고 표시를 해둔다고 생각하면 됨 + 자식 객체는 삭제되지 않음
- 만약 Cascade나 OrphanRemoval을 같이 사용한다면 삭제하게 할 수 있음
- Soft Delete를 한다면, 데이터가 실제 DB에는 남아있어 복구하기 쉽다는 장점이 있지만, 데이터가 남아있기 때문에 용량에 관한 문제가 발생할 수 있음
Soft Delete 사용 방법
- 만약 Parent 객체에만 Soft Delete를 적용한다고 한다면 아래와 같이 Parent 클래스만 수정
@Entity
@SQLDelete(sql = "UPDATE parent SET deleted_at = current_timestamp WHERE id = ?")
@Where(clause = "deleted_at is null")
public class Parent {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private int age;
private LocalDateTime deletedAt;
@OneToMany(mappedBy = "parent")
List<Child> children = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "grand_parent_id")
private GrandParent grandParent;
}
- 위와 같이 수정 후 parent를 삭제한다면(deleteById) 아래와 같이 데이터는 남아있지만 deletedAt이 채워짐
- 이 상황에서는 Parent의 삭제는 정상적으로 가능하지만, Child, GrandParent에서 Parent를 참조하거나, id가 2인 Parent를 조회하면 에러가 발생함
반응형
'Trouble Shooting' 카테고리의 다른 글
[RDS MySQL] Too many connections 해결 방법 (0) | 2023.05.03 |
---|---|
[Spring Boot] 프로젝트 TimeZone 설정 (0) | 2023.04.19 |
[Spring Boot] Jpa로 MySQL 테이블 생성 안될때 해결 방법 (0) | 2023.01.14 |