Persistence Framework/ORM -JPA

영속성 전이 : CASCADE 와 고아객체

prden 2021. 8. 29. 22:23

 

1. 영속성 전이가 사용되는 경우?

부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수 있도록 하게 하려면 영속성 전이 기능을 사용해야 한다. 

 

예를 들어 부모 엔티티가 Order이고 자식 엔티티가 OrderDetails(or OrderLines) 일 때

1) Order의 경우 ( order- orderDetails 부분을 보자)


@Entity
@Table(name = "\"order\"")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order extends BaseTimeEntity {

    @Id @GeneratedValue
    @Column(name = "order_id")
    private long orderId;

    @Enumerated(EnumType.STRING)
    private OrderStatus orderStatus;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private DeliveryStatus deliveryStatus;

    @Column(name="total_price")
    private int totalPrice;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id")
    private Account account;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderDetails> orderDetails= new ArrayList<>();

    @OneToOne
    @JoinColumn(name = "order_exchange_refund_id")
    private OrderExchangeRefund orderExchangeRefund;


    private void calculateTotalPrice(){
        this.totalPrice = this.orderDetails.stream()
                .mapToInt(orderItem -> orderItem.getOrderProductPrice())
                .sum();
    }

    @Builder
    public Order(Account account, List<OrderDetails> orderDetailsList, Address address){
        this.account = account;
        this.address = address;
        this.setOrderProductsList(orderDetailsList);
        this.orderStatus = OrderStatus.ORDERED;
        this.deliveryStatus =DeliveryStatus.PENDING;
    }

    private void setOrderProductsList(List<OrderDetails> orderDetailsList) {
        orderDetailsList.stream()
                .forEach(orderDetails -> this.orderDetails.add(orderDetails));
        
        //order 이렇게 세팅해줘야 order_id 들어간다.(영속성 전이 활용)
        orderDetailsList.stream()
                .forEach(orderDetails -> orderDetails.setOrder(this));
        this.calculateTotalAmount();
    }

    private void calculateTotalAmount() {
        this.totalPrice = this.orderDetails.stream()
                .mapToInt(orderItem -> orderItem.getOrderProductPrice())
                .sum();
    }

}

 

2) OrderDetail의 경우 

package com.example.malljpa.order;

import com.example.malljpa.common.BaseTimeEntity;
import com.example.malljpa.product.domain.Product;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

@Table(name = "order_details")
@Entity
@Getter
@NoArgsConstructor
public class OrderDetails {

    @Id @GeneratedValue
    @Column(name = "order_details_id")
    private Long orderDetailsId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne
    @JoinColumn(name = "product_id")
    private Product product;

    @Column(name = "order_quantity")
    private int orderQuantity;

    @Column(name = "order_product_price")
    private int orderProductPrice;

    @Builder
    public OrderDetails(Product product, int orderQuantity){
        this.product = product;
        this.orderQuantity =orderQuantity;
        this.calculateOrderProductPrice();
    }

    //하나의제품 여러 개 주문 할 경우 특정제품의 총합산 가격
    private void calculateOrderProductPrice(){
        this.orderProductPrice = this.product.getUnitPrice() * orderQuantity;
    }


    public void reduceStockQuantity() {
        this.product.reduceStockQuantity(orderQuantity);
    }

//연관관계 매핑 철저히
	public void setOrder(Order order) {
        this.order = order;
    }
}

3) 부모인 Order에 주문 1개가 들어가고 그에 따라 OrderDetails에 2개 이상이 들어가도록 하기 위해서는

부모만 영속 상태로 만들면된다. 그러면 연관된 자식까지 한 번에 영속 상태로 만들 수 있다. ( JPA에서는 엔티티를 저장할 때 모든 엔티티를 영속 상태로 만들어 주어야 하기 때문이다.)

Order 코드에 보이듯이 자식 -> 부모 연관관계 설정을 해주면 됨.

 

 

**양방향 연관 관계와는 별개이니 양방향 연관관계 매핑 철저히 해줘야 한다. (양방향 연관관계를 추가한 다음 영속상태로 만들어야 한다. )

private static void saveWithCascade(EntityManager em){

	Child child1 = new Child();
    Child child2 = new Child();
    
    Parent parent = new Parent();
    child1.setParent(parent); //연관관계 추가(영속성 전이와 무관, 꼭 양방향 연관관계 매핑 필요)
    child2.setParent(parent); //연관관계 추가
    parent.getChildren().add(child1);
    parnet.getChildren().add(child2);
    
    //부모저장, 하면 연관된 자식들 저장됨
    em.persist(parent);
    
 }

 

2. 삭제하는 경우 영속성 전이

부모와 자식 엔티티 모두 한 번에 제거하려면 각각 제거할 때는 아래와 같이해야 한다.

Parent findParent = em.find(Parent.class, 1L);

Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);

em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);

//만약 영속성을 이용해서 한 번에 제거한다면

Parnet findParent = em.find(Parent.class, 1L);
em.remove(findParnet);

//이 경우 Delete SQL을 총 3번 실행하면서 부모1, 자식2 다 삭제해 준다. (제약조건 고려해서 자식 먼저 삭제 후 부모 삭제함)

 

3. 고아 객체

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공한다. 이것을 고아객체 제거라고 한다. 

이 경우 부모 엔티티의 컬렉션에서 자식 엔티티의 '참조만' 제거하면 자식 엔티티가 자동으로 삭제된다. 

 

@Entity
public class Parent{
	
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy ="parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<>();
    
 }

위와 같을 때

 
 Parent parent1 = em.find(Parent.class, id);
 parent1.getChilderen().remove(0); // 자식 엔티티를 컬렉션에서 제거
 
 실행 SQL은 다음과 같다. 
 
 DELETE FORM CHILD WHERE ID =?
 
 만약 모든 자식 엔티티를 제거하고자 한다면
 parent1.getChildren().clear();

이러한 동작이 작동하는 이유는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 것이다. 이 기능은 참조하는 곳이 하나인 경우에만 해야 한다. 즉, 특정 엔티티가 개인이 소유하는 엔티티에서만 이 기능을 적용해야 한다. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생한다.  따라서 orphanRemoval은 @OneToOne, @OneToMany에서만 사용한다. 

 

4. 결론 (영속성 전이와 고아객체 를 한 번에 사용하면?)

CascadeType.ALL +orphanRemoval = true를 동시에 사용한다면

EntityManager.persist()를 통해 영속화되고 EntityManager.remove()를 통해 제거된다. 이 두 가지를 동시에 활성화한다면 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다. 예를 들어

자식을 저장하기 위해서 부모에 등록만 해주면 된다. (CASCADE)

 

Parent parent = em.find(Parent.class, parentId);

parent.addChild(child1);

 

자식을 삭제하려면 부모에서 제거하면 된다(orphanRemoval)

Parnet parent = em.find(Parent.class, parentId);

parent.getChildren().remove(removeObject);

 

 

 

'Persistence Framework > ORM -JPA' 카테고리의 다른 글

Spring Data JPA Pagination, Querydsl의 pagenation연결  (0) 2022.10.11
QueryDsl, 사용자 정의 Repository  (0) 2022.10.11
QueryDsl 설정  (0) 2022.10.05
N+1문제  (0) 2021.08.30
영속성 컨텍스트  (0) 2021.07.02