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 |