어떤 엔티티를영속 상태로 만들 때, 연관된 엔티티도 함께 영속화 하고 싶을때,
cascade 옵션을 통해 영속성 전이를 할 수 있다.
먼저 부모 엔티티를 영속화 할때 CascadeType 옵션을 주면 되는데, 아래의 6가지 옵션이 있다.
|
이 중 일반적으로 사용되는 ALL, PERSIST, REMOVE로 살펴본다.
하나의 Item 엔티티가 있고, 그 안에서 Option, OptionDetail 엔티티가 있는데,
Option이 부모 엔티티, OptionDetail 엔티티가 자식 엔티티이다.(편의상 Item 엔티티는 제외한다)
먼저 cascade를 설정하지 않은 경우를 보자.
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "OPTION")
public class Option {
@Id
@Column(name = "OPTION_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long optionId;
@Column(name = "NAME")
private String name;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "ITEM_ID")
private Item item;
@Builder.Default
@OneToMany(mappedBy = "option" ,fetch = FetchType.LAZY)
private List<OptionDetail> optionDetails = new ArrayList<>();
public void addOptionDetail(OptionDetail optionDetail) {
optionDetails.add(optionDetail);
}
}
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "OPTION_DETAIL")
public class OptionDetail {
@Id
@Column(name = "OPTION_DETAIL_ID")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long optionDetailId;
@Column(name = "NAME")
private String name;
@Column(name = "PRICE")
private int price;
@Column(name = "STOCK_QUANTITY")
private int stockQuantity;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "OPTION_ID")
private Option option;
}
cascade 옵션이 없을 경우 테스트 코드를 통해 어떻게 쿼리가 실행되는지 로그로 확인해보자
@Test
@Transactional
void cascade() {
//given
Option option = Option.builder().name("cascade_option").build();
OptionDetail optionDetail = OptionDetail.builder().name("cascade_option_detail").price(100).stockQuantity(50).option(option).build();
option.addOptionDetail(optionDetail);
//when
optionRepository.save(option);
optionDetailRepository.save(optionDetail);
}
이제 Option 엔티티에 cascade 옵션을 주고, ItemDetail은 따로 영속화하지 않는다.
@Builder.Default
@OneToMany(mappedBy = "option" ,fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
private List<OptionDetail> optionDetails = new ArrayList<>();
@Test
@Transactional
void cascade() {
//given
Option option = Option.builder().name("cascade_option").build();
OptionDetail optionDetail = OptionDetail.builder().name("cascade_option_detail").price(100).stockQuantity(50).option(option).build();
option.addOptionDetail(optionDetail);
//when
optionRepository.save(option);
// optionDetailRepository.save(optionDetail); //제거
}
itemDetailRepository의 save()를 호출하지 않아도 option_detail 테이블에 insert 쿼리가 실행되는 것을 확인할 수 있다.
우리는 믿음이 부족하기 때문에, cascade 옵션을 제거한채로 동일한(주석 처리된) 테스트 코드를 실행해보자.
@Builder.Default
@OneToMany(mappedBy = "option" ,fetch = FetchType.LAZY) //cascade 제거
private List<OptionDetail> optionDetails = new ArrayList<>();
option 테이블에 대한 insert 쿼리만 수행되고, option_detail은 수행되지 않는 것을 확인할 수 있다.
orphanRemoval 옵션은 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 "고아 객체"라고 하는데,
이러한 고아 객체를 자동으로 삭제해주는 기능을 한다.
먼저 cascade 옵션만 있는 상태에서 부모 엔티티에서 자식 엔티티의 참조를 제거해보자.
@Autowired
private EntityManager entityManager;
@Test
@Transactional
void orphanRemoval() {
//given
Option findOption = optionRepository.findById(1L).get();
//when
findOption.getOptionDetails().remove(0);
entityManager.flush();
}
그냥 조회 쿼리만 수행되는 것을 확인할 수 있다.
이제 orphanRemoval = true 옵션을 주고 다시 테스트를 수행하자.
@Builder.Default
@OneToMany(mappedBy = "option" ,fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<OptionDetail> optionDetails = new ArrayList<>();
option_detail 테이블에서 삭제 쿼리가 실행되는 것을 확인할 수 있다.
물론 cascade와 orphanRemoval 옵션을 아무렇게나 사용하는 것은 권장하지 않는다.
1 : N : 1 관계나 @ManyToMany 관계의 경우에는, 엔티티의 경우 한쪽의 부모 엔티티의 영속화로 인해,
다른 부모 엔티티에서 자식 엔티티가 사라지는 케이스가 발생할 수 있기 때문이다.
엔티티간의 관계는 늘 복잡하고, 여러번 들어도 이렇게 정리해두지 않으면 오래 기억되지 않는 것 같다.
참고1 : https://hongchangsub.com/jpa-cascade-2/
참고 2 : https://willseungh0.tistory.com/67
참고 3 : https://resilient-923.tistory.com/417
'개발 > Jpa' 카테고리의 다른 글
@Embedded, @Embeddable (0) | 2023.05.01 |
---|---|
[JPA] JPA의 어노테이션(1) (0) | 2022.05.08 |
[Jpa]처음 접하는 Jpa (0) | 2022.02.27 |