연관 관계 매핑2 - 영속성 전이

진크·2022년 2월 26일
0
post-thumbnail

영속성 전이란?

영속성 전이(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을 삭제하는 쿼리문이 출력되는 것을 확인할 수 있습니다. 즉, 부모 엔티티와 연관 관계가 끊어졌기 때문에 고아 객체를 삭제하는 쿼리문이 실행되는 것입니다.

profile
철학있는 개발자 - 내가 무지하다는 것을 인정할 때 비로소 배움이 시작된다.

0개의 댓글