https://rogi221.tistory.com/174
Spring Boot - 쇼핑몰 프로젝트 05 (연관 관계 매핑 - 1)
연관 관계 매핑 - 1 연관 관계 매핑 종류 엔티티들은 대부분 다른 엔티티와 연관 관계를 맺으며, JPA에서는 엔티티에 연관 관계를 매핑해두고 필요할 때 해당 엔티티와 연관된 엔티티를 사용하여
rogi221.tistory.com
연관 관계 매핑 - 2
영속성 전이
- 영속성 전이 : 엔티티의 상태를 변경할 때 해당 엔티티와 연관된 엔티티의 상태 변화를 전파시키는 옵션
- 부모는 One에 해당하고 자식은 Many에 해당
- (ex) Order 엔티티(부모)가 삭제되었을 때 해당 엔티티와 연관되어 있는 OrderItem 엔티티(자식)가 함께 삭제 되거나, Order 엔티티를 저장 할때 Order 엔티티에 담겨있던 OrderItem 엔티티를 한꺼번에 저장
- 영속성 전이 옵션
- 영속성 전이 옵션은 단일 엔티티에 완전히 종속적이고 부모 엔티티와 자식 엔
- 티티의 라이프 사이클이 유사할 때 cascade 옵션을 활용하기를 추천
- 무분별하게 사용할 경우 삭제되면 안될 데이터도 삭제될 수 있음
주문 영속성 전이 테스트하기
com.shop.repository.OrderRepository.java
// OrderRepository.java
package com.shop.repository;
import com.shop.entity.Order;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderRepository extends JpaRepository<Order, Long> {
}
com.shop.entity.Order.java
// Order.java
... 기존 임포트 생략 ...
@Entity
@Table(name="orders")
@Getter
@Setter
public class Order {
... 생략 ...
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
}
com.shop.entity.OrderTest.java
// OrderTest.java
package com.shop.entity;
import com.shop.constant.ItemSellStatus;
import com.shop.repository.ItemRepository;
import com.shop.repository.OrderRepository;
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.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest
@TestPropertySource(locations="classpath:application-test.properties")
@Transactional
public class OrderTest {
@Autowired
OrderRepository orderRepository;
@Autowired
ItemRepository itemRepository;
@PersistenceContext
EntityManager em;
public Item createItem() {
Item item = new Item();
item.setItemNm("테스트 상품");
item.setPrice(10000);
item.setItemDetail("상세설명");
item.setItemSellStatus(ItemSellStatus.SELL);
item.setStockNumber(100);
item.setRegTime(LocalDateTime.now());
item.setUpdateTime(LocalDateTime.now());
return item;
}
@Test
@DisplayName("영속성 전이 테스트")
public void cascadeTest() {
Order order = new Order();
for(int i=0; i<3; i++){
Item item = this.createItem();
itemRepository.save(item);
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setCount(50);
orderItem.setOrderPrice(1000);
orderItem.setOrder(order);
order.getOrderItems().add(orderItem);
}
orderRepository.saveAndFlush(order);
em.clear();
Order savedOrder = orderRepository.findById(order.getId())
.orElseThrow(EntityNotFoundException::new);
assertEquals(3, savedOrder.getOrderItems().size());
}
}
- 테스트 코드 실행 결과 실제 조회되는 orderItem이 3개이므로 테스트가 정상적으로 통과
고아 객체 제거
- 고아 객체 : 부모 엔티티와 연관 관계가 끊어진 자식 엔티티
- 영속성 전이 기능과 같이 사용하면 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있음
- 고아객체 제거 기능은 참조하는 곳이 하나일 때만 사용
- OrderItem 엔티티를 Order 엔티티가 아닌 다른 곳에서 사용하고 있다면 이 기능을 사용하면 안됨
- @OneToOne, @OneToMany 어노테이션에서 “orphanRemoval = true” 옵션 사용
고아 객체 제거 테스트하기
com.shop.entity.Order.java
// Order.java
public class Order {
... 생략 ...
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItem> orderItems = new ArrayList<>();
}
com.shop.entity.OrderTest.java
// OrderTest.java
... 기존 임포트 생략 ...
import com.shop.repository.MemberRepository;
public class OrderTest {
... 생략 ...
@Autowired
MemberRepository memberRepository;
public Order createOrder() {
Order order = new Order();
for(int i=0; i<3; i++){
Item item = this.createItem();
itemRepository.save(item);
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setCount(10);
orderItem.setOrderPrice(1000);
orderItem.setOrder(order);
order.getOrderItems().add(orderItem);
}
Member member = new Member();
memberRepository.save(member);
order.setMember(member);
orderRepository.save(order);
return order;
}
@Test
@DisplayName("고아객체 제거 테스트")
public void orphanRemovalTest(){
Order order = this.createOrder();
order.getOrderItems().remove(0);
em.flush();
}
}
- flush()를 호출하면 콘솔창에 orderItem을 삭제하는 쿼리문이 출력되는 것을 확인 가능
- 부모 엔티티와 연관 관계가 끊어졌기 때문에 고아 객체를 삭제하는 쿼리문이 실행됨
즉시 로딩의 문제점
- 조인해서 가져오는 엔티티가 많아질수록 쿼리가 어떻게 실행될지 개발자가 예측하기 힘듬
- 사용하지 않는 엔티티도 한꺼번에 가져오기 때문에 성능상 문제가 발생할 수 있음
- 즉시 로딩 대신에 지연 로딩 방식 사용
주문 엔티티 조회 테스트하기(즉시 로딩)
com.shop.repository.OrderItemRepository.java
// OrderItemRepository.java
package com.shop.repository;
import com.shop.entity.OrderItem;
import org.springframework.data.jpa.repository.JpaRepository;
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
}
com.shop.entity.OrderTest.java
// OrderTest.java
... 생략 ...
@Autowired
OrderItemRepository orderItemRepository;
@Test
@DisplayName("지연 로딩 테스트")
public void lazyLoadingTest() {
Order order = this.createOrder();
Long orderItemId = order.getOrderItems().get(0).getId();
em.flush();
em.clear();
OrderItem orderItem = orderItemRepository.findById(orderItemId)
.orElseThrow(EntityNotFoundException::new);
System.out.println("Order class : " + orderItem.getOrder().getClass());
}
}
- 즉시 로딩 사용시 연관된 모든 객체를 가져오기 때문에 성능상 문제가 발생할 수 있음
com.shop.entity.OrderItem.java
// OrderItem.java
package com.shop.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@Setter
public class OrderItem {
@Id
@GeneratedValue
@Column(name="order_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY) << 추가
@JoinColumn(name="item_id")
private Item item;
@ManyToOne(fetch = FetchType.LAZY) << 추가
@JoinColumn(name="order_id")
private Order order;
private int orderPrice; // 주문가격
private int count; // 수량
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
- 즉시 로딩 대신에 지연 로딩 방식으로 변경
com.shop.entity.OrderTest.java
// OrderTest.java
... 생략 ...
@Autowired
OrderItemRepository orderItemRepository;
@Test
@DisplayName("지연 로딩 테스트")
public void lazyLoadingTest() {
Order order = this.createOrder();
Long orderItemId = order.getOrderItems().get(0).getId();
em.flush();
em.clear();
OrderItem orderItem = orderItemRepository.findById(orderItemId)
.orElseThrow(EntityNotFoundException::new);
System.out.println("Order class : " + orderItem.getOrder().getClass());
System.out.println("==============================="); <<<
orderItem.getOrder().getOrderDate(); <<< 추가
System.out.println("==============================="); <<<
}
}
- 테스트 코드 실행 결과 orderItem 엔티티만 조회하는 쿼리문이 실행되는 것을 볼 수 있음
- 테스트 코드의 실행 결과 Order 클래스 조회 결과가 HibernateProxy라고 출력되는 것을 볼 수 있음
- 지연 로딩으로 설정하면 실제 엔티티 대신에 프록시 객체를 넣어둠
- 프록시 객체는 실제로 사용되기 전까지 데이터 로딩을 하지 않고, 실제 사용 시점에 조회 쿼리문이 실행
- Order의 주문일(orderDate)을 조회할 때 select 쿼리문이 실행되는 것을 확인 가능
- 인텔리제이에서 @OneToMany 어노테이션을 <Ctrl>+마우스로 클릭하면 @OneToMany 어노테이션의 경우는 기본 FetchType이 LAZY 방식으로 되어 있는 것을 확인할 수 있음
- 어떤 어노테이션은 즉시 로딩이고, 어떤 어노테이션은 지연 로딩인데 사람인 이상 헷갈릴 수 있기 때문에 연관 관계 매핑 어노테이션에 Fetch 전략을 LAZY로 직접 설정
엔티티 지연 로딩 설정
com.shop.entity.Cart.java
// Cart.java
... 기존 임포트 생략 ...
public class Cart {
@Id
@Column(name ="cart_id")
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@OneToOne(fetch = FetchType.LAZY) << 추가
@JoinColumn(name="member_id")
private Member member;
}
com.shop.entity.CartItem.java
// CartItem.java
package com.shop.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
@Entity
@Getter
@Setter
@Table(name="cart_item")
public class CartItem {
@Id
@GeneratedValue
@Column(name="cart_item_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY) << 수정
@JoinColumn(name="cart_id")
private Cart cart;
@ManyToOne(fetch = FetchType.LAZY) << 수정
@JoinColumn(name="item_id")
private Item item;
private int count;
}
com.shop.entity.Order.java
// Order.java
package com.shop.entity;
import com.shop.constant.OrderStatus;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name="orders")
@Getter
@Setter
public class Order {
@Id
@GeneratedValue
@Column(name="order_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="member_id")
private Member member;
private LocalDateTime orderDate; // 주문일
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus; // 주문상태
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL,
orphanRemoval = true, fetch = FetchType.LAZY) << 수정
private List<OrderItem> orderItems = new ArrayList<>();
private LocalDateTime regTime;
private LocalDateTime updateTime;
}
https://rogi221.tistory.com/176
Spring Boot - 쇼핑몰 프로젝트 05 (연관 관계 매핑 - 3)
https://rogi221.tistory.com/174 Spring Boot - 쇼핑몰 프로젝트 05 (연관 관계 매핑 - 1) 연관 관계 매핑 - 1 연관 관계 매핑 종류 엔티티들은 대부분 다른 엔티티와 연관 관계를 맺으며, JPA에서는 엔티티에 연
rogi221.tistory.com
'Spring boot & JPA' 카테고리의 다른 글
Spring Boot - 쇼핑몰 프로젝트 06 (상품 등록 및 조회하기 - 1) (0) | 2023.04.05 |
---|---|
Spring Boot - 쇼핑몰 프로젝트 05 (연관 관계 매핑 - 3) (0) | 2023.04.05 |
Spring Boot - 쇼핑몰 프로젝트 05 (연관 관계 매핑 - 1) (0) | 2023.04.05 |
Spring Boot - 쇼핑몰 프로젝트 04 (스프링 시큐리티를 이용한 회원 가입 및 로그인 - 2) (0) | 2023.04.05 |
Spring Boot - 쇼핑몰 프로젝트 04 (스프링 시큐리티를 이용한 회원 가입 및 로그인 - 1) (0) | 2023.04.04 |