- 문제
여러 데이터를 삭제할 때 @Query와 @Modifying를 사용하는 벌크 연산을 통해 성능 최적화를 시도했으나 SQLIntegrityConstraintViolationException 예외와 함께 삭제가 진행되지 않는 상황이 발생했다.
[DataBase - Table] shiba
[DataBase - Table] shiba_house
[Entity] Shiba
@Entity
@Table(name = "shiba")
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Shiba extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
public Shiba(String name, Integer age) {
this.name = name;
this.age = age;
}
public void updateAge(Integer age) {
this.age = age;
}
@OneToOne(mappedBy = "shiba", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private House house;
}
[Entity] House
@Entity
@Table(name = "shiba_house")
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class House {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Enumerated(EnumType.STRING)
private Color color;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "shiba_id")
private Shiba shiba;
public House(String name, Color color, Shiba shiba) {
this.name = name;
this.color = color;
this.shiba = shiba;
}
}
Shiba 엔티티와 House 엔티티는 1:1 연관관계를 가지고 있으며, 연관관계의 주인은 House이다.
[Repository] ShibaRepository
@Modifying
@Query("delete from Shiba s where s.id in (:idList)")
int deleteAllByIdIn(@Param("idList") List<Long> idList);
@Modifying과 @Query를 활용해 삭제 대상에 해당하는 id와 일치하는 Shiba 데이터를 삭제한다.
[Test]
@Test
@DisplayName("해당되는 시바견 데이터를 삭제한다.")
@Transactional
void deleteAllTargetShiba() {
List<Shiba> target = shibaRepository.findByIdIn(Arrays.asList(1L,2L,3L,4L,5L,6L,7L,8L,9L,10L));
List<Long> removeTargetShiba = target.stream()
.map(Shiba::getId)
.collect(Collectors.toList());
shibaRepository.deleteAllByIdIn(removeTargetShiba);
}
테스트를 통해 해당하는 시바견 데이터를 삭제하려고 하자 다음과 같은 예외 원인이 출력되고 테스트에 실패하게 되었다.
Caused by: java.sql.SQLIntegrityConstraintViolationException: (conn=295) Cannot delete or update a parent row: a foreign key constraint fails (`blog`.`shiba_house`, CONSTRAINT `FKh17f1oq06jpatjct3jrd0yask` FOREIGN KEY (`shiba_id`) REFERENCES `shiba` (`id`))
- 원인
@Query + @Modifying 사용에 익숙지 않은 개발자들은 엔티티의 연관관계의 설정만 보고 쉽게 CascadeType을 ALL 또는 Remove로 설정해 놨으니, JPA가 알아서 매핑된 테이블의 데이터를 잘 삭제해 주겠지라고 판단할 수 있다.
하지만 @Query + @Modifying을 활용한 벌크 연산을 할 때는 데이터베이스에 직접 쿼리 한다는 특징이 있으므로 삭제에 실패한다.
벌크 연산은 영속성 콘텍스트와 2차 캐시를 무시하고 데이터베이스에 직접 쿼리 한다. 그러므로 엔티티 설정보다 데이터베이스 설정에 더 관계가 가깝다는 사실에 주의해야 한다.
- 해결
이 문제를 해결하는 데는 데이터베이스에서 제약사항을 직접 변경하는 방법과 연관된 데이터를 먼저 삭제하는 방법 2가지가 존재한다. 먼저 데이터베이스에서 직접 제약사항 설정을 변경하는 방법부터 알아보자.
1. 직접 제약사항 설정 변경하기
테이블의 제약사항을 변경하기 위해서는 먼저 기존에 있었던 foreign key를 제거하고 다시 추가하는 방식으로 진행해야 한다. 기본 제약사항 변경 문법은 다음과 같다.
ALTER TABLE [테이블 이름] DROP FOREIGN KEY [foreignkey 이름];
ALTER TABLE [테이블 이름] ADD CONSTRAINT [foreignkey 이름] FOREIGN KEY [자식 속성] REFERENCES [부모 테이블 이름](자식 속성이 참고할 부모 속성) ON DELETE CASCADE;
복잡하다면 예시를 참고하자.
예시로 shiba 테이블과 shiba_house 테이블 기준에서 변경하는 SQL문은 다음과 같다.
ALTER TABLE `shiba_house` DROP FOREIGN KEY `FKh17f1oq06jpatjct3jrd0yask`;
ALTER TABLE `shiba_house` ADD CONSTRAINT `FKh17f1oq06jpatjct3jrd0yask` FOREIGN KEY (`shiba_id`) REFERENCES `shiba` (`id`) ON DELETE CASCADE;
제약사항 변경 후 테스트를 다시 실행하면 성공적으로 삭제가 되는 것을 확인할 수 있다.
결과는 성공이지만, 개인적으로 데이터베이스의 설정을 수정해야 하는 해당 방법은 추천하지는 않는다.
정말 이런 식으로 해결하는 경우가 필요하다면 반드시 팀원 및 DBA와 충분히 상의한 후 진행하도록 하자.
2. 직접 연관관계 맺어진 데이터를 우선적으로 삭제하기 (연관관계 주인 테이블 먼저 삭제하기)
해당 문제가 발생하는 이유는 shiba 테이블에서 데이터를 삭제하는 경우 shiba_house 테이블의 외래키 제약사항에 위배되기 때문이다. 따라서 shiba_house의 데이터를 우선적으로 지워주면 해결이 가능하다.
@Test
@DisplayName("해당되는 시바견 데이터를 삭제한다.")
@Transactional
void deleteAllTargetShiba() {
List<Shiba> target = shibaRepository.findByIdIn(Arrays.asList(1L,2L,3L,4L,5L,6L,7L,8L,9L,10L));
List<Long> removeTargetShibaHouse = target.stream()
.map(Shiba::getHouse)
.map(House::getId).collect(Collectors.toList());
houseRepository.deleteAllByIdIn(removeTargetShibaHouse);
List<Long> removeTargetShiba = target.stream()
.map(Shiba::getId)
.collect(Collectors.toList());
shibaRepository.deleteAllByIdIn(removeTargetShiba);
}
1. Shiba 엔티티와 연결된 House 엔티티를 먼저 찾아서 삭제한다.
2. 삭제 대상인 Shiba 데이터를 삭제한다.
예시는 굉장히 간단했지만 실제로 개발할 때는 아마 훨씬 더 많은 연관관계가 얽혀 있을 것이다. 특정 데이터를 삭제한다면 관련된 연관관계 A - 연관관계 B - 연관관계 C... 즉, 많은 연관관계의 체이닝과 우선순위를 잘 파악해서 삭제해야 한다.
'Spring > JPA' 카테고리의 다른 글
Spring Boot 3.0 QueryDsl Maven 설정하기 (0) | 2023.04.09 |
---|---|
JPA 동일 트랜잭션에서 update와 insert 동시에 수행할 때 문제 해결하기 (쿼리 실행 순서 문제) (0) | 2023.02.05 |
JPA @Where 일괄 조회 사용법 및 문제점과 해결방안 (0) | 2023.01.24 |
JPA @Query @Modifying 벌크 연산시 자동 업데이트(Auditing) 주의사항 (0) | 2023.01.15 |
JPA, ORM 그리고 패러다임의 불일치 (0) | 2022.07.24 |
댓글