코드 그라데이션

shop 구현 (4) 연관관계 매핑 본문

Spring/SpringShop

shop 구현 (4) 연관관계 매핑

완벽한 장면 2023. 7. 11. 12:08

스프링 부트 쇼핑몰 프로젝트 with JPA

 

카트와 멤버 -> 상식에 기대어 One To One 관계

 

외래키

=> 상대방의 키를 내가 들고 있다 = 외래키를 가지고 있다.

=> 상대방의 키가 내게 외래키

컨벤션은 정해져 있지만, 명시적으로 적어놓으면 좋다.

 

데이터베이스 세상에서는 key 값만 외래키로 들고있다면, 객체세상에서는 엔티티 자체를 가진다.

(JPA의 관여)

 

@Entity
@Getter
@Setter
@Table(name = "cart_item")
public class CartItem {

  @Id
  @GeneratedValue
  @Column(name = "cart_item_id")
  private Long id;

  @ManyToOne
  @JoinColumn(name = "cart_id")
  private Cart cart;
// 카트 아이템이 many, 카트가 one
// 카트 아이템 여러개가 하나의 카트에 있을 수 있다는 뜻.



  @ManyToOne
  @JoinColumn(name = "item_it")
  Item item;

  private int count;

}

 

 

Item 추가

  @ManyToMany
  @JoinTable (
          name = "member_item",
          joinColumns = @JoinColumn(name = "member_id"),
          inverseJoinColumns = @JoinColumn(name = "item_id")
  )
  private List<Member> member;

- 한 쪽에만 Many To Many 적어놓고, 한 쪽에는 연결을 안 해도 된다.(정보를 몰라도 됨)

=> 단방향.

 

단방향 양방향은 일대다, 다대다, 일대일과는 무관하다.

 

예를 들어 Cart와 CartItem의 관계

장바구니는 장바구니아이템을 가져오는 경우가 많다.

그런데 장바구니아이템 입장에서는 장바구니가 무엇인지(어디에 속했는지) 별로 상관이 없다.

이럴 때 One to Many와 별개로 카트만 카트아이템의 방향을 설정해주면 된다.

 

ex2. 스터디 팀과 멤버의 관계

팀을 조회할 때 멤버를 자연스레 알아야 한다라고 하면 방향성 설정

멤버를 조회할 때 속한 팀도 함께 알고 싶다고 하면 방향성 설정

=> 이러면 양방향 연관관계가 되는 것이다.

(애플리케이션의 필요에 의해 방향성 설정)

 

방향 설정을 아예 하지 않을 수도 있다.

 

데이터베이스 세상과 애플리케이션 세상을 연결해주는 애가 누구냐.

ORM 기술. JPA

RDB를 보고 애플리케이션을 만들 수도 있고

애플리케이션을 보고 RDB를 만들 수도 있다.

그리고 이 둘 사이의 불일치를 해결해주는 것이 JPA라고 하는데,

이 둘 간의 독립성을 완벽하게 보장(즉, 상관없이 만들고 싶다) 하고싶다 하면, @One To Many 같은 어노테이션을 붙이지 않으면 JPA는 연결을 시켜주지 않는다.

ManyToMany

Many To Many 는 JoinTable을 하나 둬서 두 개의 One To Many로 우회해서 표현하는 게 일반적이다.

 

 

Order

@Entity
@Table(name = "orders")
@Getter
@Setter
public class Order {

  @Id
  @GeneratedValue
  @Column(name = "order_id")
  private Long id;

  @ManyToOne
  @JoinColumn(name = "member_id")
  private Member member;
  // 한 명의 회원이 여러번 주문을 할 수 있으므로

  private LocalDateTime orderDate;

  @Enumerated(EnumType.STRING)
  private OrderStatus orderStatus;

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

  private LocalDateTime regTime;

  private LocalDateTime updateTime;
}

엔티티와 엔티티 아닌 것을 어떻게 구분하느냐.

엔티티는 자기만의 고유한 식별자를 가지면서, 자기만의 라이프사이클을 갖는다.

예를 들어 위에 OrderStatus 같은 경우, 혼자서 존재할 수 없다.

주문이 존재해야만 존재할 수 있는 값들이므로 엔티티가 될 수 없음.


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

 

객체 세상에서 양방향을 어떻게 편하게 관리할거냐

주인(관리하는 애) - 부하 관계를 두면 더 편하다.

맵드바이가 누가 주인인지를 표현. 이걸 설정해주지 않으면 양방향이 아니라 두 개의 단방향이 생긴다.

 

원투매니 관계에서는 보통 매니가 지배를 한다.

 


Cascade 옵션

Order과 OrderItem 이 있으면 각각 따로 저장해야 하는데, Order에 OrderItem 3개가 달려있으면 Order을 저장하면서 동시에 OrderItem도 연달아 저장할 수 있게 만들어주는 옵션. 연쇄적 처리의 편리성

 

OrderItem에서

@ManyToOne 

@JoinColumn(name = "order_id") 

private Order order; 

이렇게 되어있으면,

 

Order에서

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL) 

private List<OrderItem> orderItems = new ArrayList<>();

이렇게 mappedBy 까지 설정을 해줘야 완전한 양방향 연관관계가 완성이 된다.

 

그냥 쉽게 mappedBy가 있으면 양방향이고, 없으면 두 개의 단방향이라고 생각하자(없으면 서로가 서로를 인식하지 못함)

 

이런 양방향 단방향이나 일대일 일대다 다다다 판단은 인간의 보편적 생각 속에서 설정한다.


JPA 리파지토리를 상속받은 것만으로도 빈이 된다.

 


@Test

@DisplayName("장바구니 회원 엔티티 매핑 조회 테스트")

public void findCartAndMemberTest() {

  Member member = createMember();

  memberRepository.save(member);

  Cart cart = new Cart(); 

  cart.setMember(member);

  cartRepository.save(cart); em.flush();

// 영속성 컨텍스트에 데이터를 저장 후 트랜잭션이 끝날 때 flush() 호출하여 데이터베이스에 반영

  em.clear(); // 영속성 컨텍스트에 조회 후 엔티티가 없을 경우 데이터베이스를 조회, 영속성 컨텍스트를 비워준다.

 

  Cart savedCart = cartRepository.findById(cart.getId()).orElseThrow(EntityNotFoundException::new);       

  assertEquals(savedCart.getMember().getId(), member.getId());

}

 

그림으로 보면

코드에 붙어있는 트랜잭셔널은 롤백을 시키지 않으나

 

트랜잭션이 Test에 붙어있다면, 롤백이 디폴트

다른 결과에 영향을 주지 않음.

트랜잭션은 마지막에 몰아서 날아감.

 

여기서는 바로 저장여부를 확인하기 위해서 em.flush를 날림

그러나 이것만으로 내용이 사라지지 않음.

그래서 clear까지 시킴

 

이런 구조

 

@Test
  @DisplayName("영속성 전이 테스트")
  public void cascadeTest() {
    Order order = new Order();
    for(int i = 0; i<3; i++) { // 3번 저
      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);
    }
    orderRepository.saveAndFlush(order);
    em.clear();
    Order savedOrder = orderRepository.findById(order.getId()).orElseThrow(EntityNotFoundException::new);
    assertEquals(3, savedOrder.getOrderItems().size());
  }

 

그림으로 보면

즉, 다시 코드로 보

    orderRepository.saveAndFlush(order); // 분명히 order만 저장을 했고
    em.clear();
    Order savedOrder = orderRepository.findById(order.getId()).orElseThrow(EntityNotFoundException::new);

// order를 다시 조회해봤는데,
    assertEquals(3, savedOrder.getOrderItems().size()); // orderItem에도 3개가 잘 들어가 있다를 확인.

 

즉, order만 저장된 게 아니라 orderItem까지 잘 저장되었구나를 확하는 코드가 됨.

728x90
Comments