영속성 전이(cascade)란 엔티티의 상태를 변경할 떄 해당 엔티티와 연관된 엔티티의 상태 변화를 전파하는 옵션입니다. 이때 부모는 One에 해당하고 해당 자식은 Many에 해당합니다. 영속성 전이 옵션을 부분별하게 사용할 경우 삭제되지 말아야 할 데이터가 삭제될 수 있으므로 조심해서 사용해야 합니다.(엔티티 간의 라이프사이클을 잘잘 파악할 것!)
CASCADE 종류 | 설명 |
---|---|
PERSIST | 부모 엔티티가 영속화될 때 자식 엔티티도 영속화 |
MERGE | 부모 엔티티가 병합될 떄 자식 엔티티도 병합 |
REMOVE | 부모 엔티티가 삭제될 때 연관된 자식 엔티티도 삭제 |
REFRESH | 부모 엔티티가 refresh되면 연관된 자식 엔티티도 refresh |
DETACH | 부모 엔티티가 detach 되면 연관된 자식 엔티티도 detach 상태로 변경 |
ALL | 부모 엔티티의 영속 상태 변화를 자신 엔티티에 모두 전이 |
영속성 전이 테스트를 위해 OrderRepository
인터페이스를 생성합니다.
package me.jincrates.gobook.domain.orders;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderRepository extends JpaRepository<Order, Long> {
}
package me.jincrates.gobook.domain.orders;
//...기존 임포트 생략
@Getter @ToString
@NoArgsConstructor
@Table(name = "orders")
@Entity
public class Order {
//...코드 생략
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
}
CascadeType.ALL
옵션을 설정하겠습니다.package me.jincrates.gobook.domain.orders;
import me.jincrates.gobook.domain.items.Item;
import me.jincrates.gobook.domain.items.ItemRepository;
import me.jincrates.gobook.domain.items.ItemSellStatus;
import me.jincrates.gobook.domain.members.MemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.EntityNotFoundException;
import javax.persistence.PersistenceContext;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class OrderTest {
@Autowired
OrderRepository orderRepository;
@Autowired
ItemRepository itemRepository;
@Autowired
MemberRepository memberRepository;
@PersistenceContext
EntityManager em;
public Item createItem() {
Item item = Item.builder()
.itemNm("테스트 상품")
.price(10000)
.itemDetail("테스트 상품 상세설명")
.itemSellStatus(ItemSellStatus.SELL)
.stockNumber(100)
.build();
return item;
}
@Test
@DisplayName("영속성 전이 테스트")
public void cascadeTest() {
Order order = new Order();
IntStream.rangeClosed(1, 3).forEach(i -> {
Item item = this.createItem();
itemRepository.save(item);
OrderItem orderItem = OrderItem.builder()
.item(item)
.count(10)
.orderPrice(1000)
.order(order)
.build();
order.getOrderItems().add(orderItem);
});
orderRepository.saveAndFlush(order);
em.clear();
Order savedOrder = orderRepository.findById(order.getId())
.orElseThrow(EntityNotFoundException::new);
assertEquals(10, savedOrder.getOrderItems().size());
}
}
order.getOrderItems().add(orderItem)
: 아직 영속성 컨텍스트에 저장되지 않은 orerItem 엔티티를 order 엔티티에 담아줍니다.orderRepository.saveAndFlush(order)
: order 엔티티를 저장하면서 강제로 flush를 호출하여 영속성 컨텍스트에 있는 객체들을 데이터베이스에 반영합니다.assertEquals(3, savedOrder.getOrderItems().size())
: itemOrder 엔티티 3개가 실제로 데이터베이스에 저장되었는지를 검사합니다.고아 객체란 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 말합니다. 영속성 전이 기능과 같이 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있습니다. 고아 객체 제거 기능은 참조하는 곳이 하나일 때만 사용해야 합니다. 다른 곳에서도 참조하고 있는 엔티티인데 삭제하면 문제가 생길 수 있기 때문입니다.
package me.jincrates.gobook.domain.orders;
//...기존 임포트 생략
@Getter @ToString
@NoArgsConstructor
@Table(name = "orders")
@Entity
public class Order {
//...코드 생략
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems = new ArrayList<>();
}
package me.jincrates.gobook.domain.orders;
//...기존 임포트 생략
import me.jincrates.gobook.domain.members.MemberRepository;
@SpringBootTest
@Transactional
@TestPropertySource(properties = {"spring.config.location=classpath:application-test.yml"})
public class OrderTest {
//...코드 생략
@Autowired
MemberRepository memberRepository;
public Order createOrder() {
Order order = new Order();
IntStream.rangeClosed(1, 3).forEach(i -> {
Item item = this.createItem();
itemRepository.save(item);
OrderItem orderItem = OrderItem.builder()
.item(item)
.count(10)
.orderPrice(1000)
.order(order)
.build();
order.getOrderItems().add(orderItem);
});
Member member = new Member();
memberRepository.save(member);
order.builder()
.member(member)
.build();
orderRepository.save(order);
return order;
}
@Test
@DisplayName("고아 객체 제거 테스트")
public void orphanRemovalTest() {
Order order = this.createOrder();
order.getOrderItems().remove(0);
em.flush();
}
}
order.getOrderItems().remove(0)
: order 엔티티에서 관리하고 있는 orderItem 리스트의 0번째 인덱스 요소를 제거합니다.flush()
를 호출하면 콘솔창에 orderItem을 삭제하는 쿼리문이 출력되는 것을 확인할 수 있습니다. 즉, 부모 엔티티와 연관 관계가 끊어졌기 때문에 고아 객체를 삭제하는 쿼리문이 실행되는 것입니다.