스프링부트 쇼핑몰 프로젝트 | 5 연관관계 맵핑(연관관계, 영속성 전이)

Yunny.Log ·2022년 5월 18일
0

Spring Boot

목록 보기
49/80
post-thumbnail

5.1 연관관계 매핑 종류

1) 일대일
2) 일대다
3) 다대일
4) 다대다

  • 테이블에서 관계는 항상 양방향, but 객체에선 단방향과 양방향이 존재

5.1.1 일대일 단방향 매핑

장바구니 - 회원

@Entity
@Table(name = "cart")
@Getter @Setter
@ToString
public class Cart extends BaseEntity {

    @Id
    @Column(name = "cart_id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="member_id")
    private Member member;

    public static Cart createCart(Member member){
        Cart cart = new Cart();
        cart.setMember(member);
        return cart;
    }

}
  • @OneToOne 어노테이션으로 회원엔티티와 일대일 매핑
  • @JoinColumn 어노테이션으로 매핑할 외래키 지정 - 컬럼명 커스텀 지정 가능
    • Cart는 member_id 컬럼을 외래키로 갖게 된다.
  • 테이블을 먼저 생성하는 쿼리문 실행, member_id를 foreign key로 지정하는 쿼리문 실행

- JPA는 영속성 컨텍스트에 데이터를 저장 후 트랜잭션 끝날 때 FLUSH() 호출해서 데이터베이스에 반영

- JPA는 영속성 컨텍스트로부터 엔티티 조회 후 영속성 컨텍스트에 엔티티가 없으면 데이터베이스 조회
  • 즉시로딩 : 엔티티 조회 시 해당 엔티티와 매핑된 엔티티도 한번에 조회하는 것
  • 일대일, 다대일로 매핑할 경우 즉시 로딩을 FETCH 전략으로 설정
  • 따로 옵션 주지 않으면 디폴트로 EAGER 값으로 전략이 설정됨

5.1.2 다대일 단방향 매핑

  • 장바구니에는 고객이 관심 있는 상품들 담아둘 것
  • 하나의 장바구니엔 여러 상품 들어가기 가능

CartItem.java

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

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

//cartitem(자신) : card = 다대일
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="cart_id")
    private Cart cart;

// caritem : item = 다대일
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "item_id")
    private Item item;

    private int count;

    public static CartItem createCartItem(Cart cart, Item item, int count) {
        CartItem cartItem = new CartItem();
        cartItem.setCart(cart);
        cartItem.setItem(item);
        cartItem.setCount(count);
        return cartItem;
    }

    public void addCount(int count){
        this.count += count;
    }

    public void updateCount(int count){
        this.count = count;
    }

}

5.1.3 다대일, 일대다 양방향 매핑

  • 양방향 매핑 : 단방향 매핑이 2개 있음 가정하면 된다
  • 위의 CartItem 엔티티는 장바구니 Cart를 참조하고 있는 단방향 매핑 상태
  • 이떄 Cart 엔티티에 장바구니 상품(CartIte) 엔티티를 참조하면 양방향 매핑이 된다.

  • 회원 - 주문 ERD
    Order.java
@Entity
@Table(name = "orders")
@Getter @Setter
public class Order extends BaseEntity {

    @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<>();

}

OrderItem.java


@Entity
@Getter @Setter
public class OrderItem extends BaseEntity {

    @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; //수량

}
  • Order에서도 Orderitem을 참조하고, Orderitem도 Order을 참조하는

  • 이를 양방향 구조라고 한다, 이떄 양방향은 연관관계의 주인을 반듯시! 설정해줘야 한다.

양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정, 안 그러면 FK가 두개나 있는 셈이니깐,
  • 연관관계의 주인만이 외래키 관리 (등록, 수정)
  • 주인이 아닌 쪽은 읽기만 가능
  • 주인은 mappedBy 속성을 사용 x
    (mappedBy는 내가 ~에 의해서 매핑 당했어 니깐 주인일 수 없지)
    주인이 아니면 mappedBy 속성으로 주인을 지정

누구를 주인으로 해야하는데?

외래키가 있는 곳을 주인으로 지정

외래키가 있는 게 多 쪽
多 쪽 이 무조건 연관관계 주인

  • 그래서 다대일 (@ManyToOne)은 mappedBy 속성이 없다고 보면 됨

5.1.4 다대다 매핑

  • 다대다 매핑은 실무에서 사용 x
  • 다대다 연결 테이블을 생성해서 다대다 관계를 일대다, 다대일 관계로 풀어내기

  • 그럼 왜 다대다 매핑을 사용하지 않을까?
    • 연결 테이블에는 컬럼을 추가할 수 없기 때문입니다.
  • 연결 테이블에는 조인 컬럼뿐 아니라 추가 컬럼들이 필요한 경우가 많다
  • 또한 엔티티를 조회할 때 member 엔티티에서 item 을 조회하면 중간 테이블이 있기 때문에 어떤 쿼리문이 실행될 지 예측이 어렵다.
  • 따라서 연결 테이블용 엔티티를 하나 생성한 뒤, 일대다- 다대일 관계로 매핑해주면 됨
  • 추가 이유 출처 : https://gilssang97.tistory.com/
  • 1) 중간 테이블을 생성해주긴 하지만 묵시적으로 생성해주기 때문에 자기도 모르는 복잡한 조인의 쿼리가 발생하는 경우가 생길 수 있다.
  • 2) 우리는 중간 테이블에 두 테이블의 기본키를 기본키이자 외래키로 들고와서 추가로 필요한 컬럼이 존재할 확률이 크지만, 중간 테이블에 필요한 추가 컬럼을 사용할 수 없다. (두 테이블에 추가된 컬럼에 대해 매핑이 되지 않기 때문이다.)

5.2 영속성 전이 ***

특정 엔티티와 연관된 엔티티의 상태를 함께 변화시키는 옵션

5.2.1 영속성 전이란?

  • 영속성 전이 : 엔티티의 상태를 변경할 때 해당 엔티티와 연관된 엔티티의 상태 변화를 전파시키는 옵션
  • 부모는 One에 해당, 자식은 Many에 해당
  • 상태가 전파되는 것을 가리킨다 (폭포와 같이 흐르는 것)

CascadeType.ALL 사용해보기

Order.java

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL
            , orphanRemoval = true, fetch = FetchType.LAZY)
    private List<OrderItem> orderItems = new ArrayList<>();
  • 부모 엔티티(Order)의 영속성 상태 변화를 자식 엔티티(OrderItem)에 모두 전이하는 옵션
  • 고객이 주문 상품 선택 후 주문 엔티티(부모) 저장할 때, 그의 자식인 주문 상품 엔티티도 저장이 되는 경우

(+) example

5.2.2 고아 객체 제거하기

  • 고아 객체 : 부모 엔티티와 연관 관계가 끊어진 자식 엔티티
  • 영속성 전이 기능과 같이 사용하면 부모 엔티티 통해서 자식 생명주기 관리 가능

고아 객체 제거 기능위한 주의사항

  • 고아 객체 제거기능은 참조하는 곳이 하나일 때만 사용
  • 다른 곳에서도 참조하는 엔티티인데 삭제하면 문제가 생길 수 있음
  • 즉 @OnetoMany, @OnetoOne 의 옵션으로 묶인 필드에게 적용할 수 있는 것

Order.java

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL
            , orphanRemoval = true, fetch = FetchType.LAZY)
    private List<OrderItem> orderItems = new ArrayList<>();
  • 이런 식으로 작성해주면 OrderItem의 부모인 Order이 삭제되면, 이에 딸린 OrderItem들도 삭제됨
  • 다만 OrderItem이 Order에게만 MAPPED BY 돼있는 것이 아니라면 이 속성 사용 금지임.

CASCADE REMOVE 옵션 VS orphanRemoval = true 옵션
1) CASCADE REMOVE

  • 기능 : 부모 엔티티가 삭제되면 자식 엔티티도 삭제
  • 부모가 자식과의 관계를 제거한다고 하더라도, 즉 자식과 부모 관계를 끊는다고 하여도 자동적으로 삭제되는 것이 아님
    If you invoke setOrders(null), the related Order entities will NOT be removed in db automatically.
    2) orphanRemoval = true
  • 기능 : 부모 엔티티 삭제되면 자식 엔티티도 삭제
  • 자식의 부모관계를 끊어버리자마자 DB에서 자동적으로 삭제됨
    setOrders(null), the related Order entities will be removed in db automatically.

5.3 지연로딩 (LAZY)

  • FETCH 전략에는 즉시 로딩 이외에도 지연로딩 전략 존재

    즉시로딩 사용 경우와, 실무에서의 부적합성

  • 일대일, 다대일로 매핑할 경우, 기본 전략인 즉시 로딩을 통해 엔티티를 함께 가져옴

  • 즉시로딩은 엔티티가 자신과 매핑된 엔티티들도(자신이 아닌 엔티티들까지;;) 한꺼번에 데려오는 것

  • 실제 비즈니스 하고 있다면 매핑되는 엔티티 갯수는 훨씬 많음

  • 개발자는 쿼리 실행 예측 불가능, 또한 대량의 데이터 한번에 조회 => 성능문제 가능성 존재
    => 따라서 lazy 타입을 사용해야 합니다.

5.4 Auditing을 이용한 엔티티 공통 속성 공통화

  • 엔티티 공통 속성으로 등록시간, 수정시간 존재

  • 실제 서비스 운영 시 등록시간, 수정시간, 등록자, 수정자를 테이블에 넣어놓고 활용 필요

  • 데이터 생성되거나 수정될 때 시간 기록 , 어떤 사용자가 등록을 했는지 아이디 남기기
    => 이 컬럼들은 버그 있거나 문의 들어올 때 활용 가능

  • 데이터 대용량으로 업데이트 진행 시, 다시 업데이트 진행해야할 경우 변경 대상 찾을 때 활용 가능하다

  • Spring Data JPA에서는 Auditinfg 기능을 제공해 엔티티가 저장, 수정될 때 자동으로 등록일, 수정일, 등록자, 수정자 입력

  • 즉 엔티티의 생성과 수정을 감시받고 있는 것
    = > 이런 공통 멤버 변수들을 추상클래스로 만들고, 해당 추상클래스를 상속받는 형태로 엔티티 리팩토링

1) AuditAware 인터페이스를 구현한 클래스 생성

  • 현재 로그인한 사용자의 정보를 등록자와 수정자로 지정하기 위해 auditaware 인터페이스 구현한 클래스 생성

AuditAwareImpl.java


public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String userId = "";
        if(authentication != null){
            userId = authentication.getName();
        }
        return Optional.of(userId);
    }

- 현재 로그인한 사용자의 정보를 조회해 사용자의 이름을 등록자와 수정자로 지정

2) Auditing 기능 사용을 위해 Config 파일 생성

AuditConfig.java

@Configuration
@EnableJpaAuditing
public class AuditConfig {

   @Bean
   public AuditorAware<String> auditorProvider() {
       return new AuditorAwareImpl();
   }

}

@Configuration 어노테이션의 정체는!?
예전부터 모르고 지나갔지만 이제야 알아본다.
출처 : https://yhmane.tistory.com/129
@Component

  • 개발자가 직접 작성한 클래스를 bean 등록하고자 할 경우 사용
    @Configuration + @Bean
  • 외부라이브러리 또는 내장 클래스를 bean으로 등록하고자 할 경우 사용.
  • 1개 이상의 @Bean을 제공하는 클래스의 경우 반드시 @Configuraton을 명시
  • 보통 테이블에 등록일, 수정일, 등록자, 수정자 다 넣어주지만 어떤 테이블은 필요없을지도 => 이런 경우엔 BaseEntity만 상속받을 수 있게 하기
  • 모든 엔티티가 상속할 baseentity는 basetimeentity를 상속받을 수 있도록 하기

BaseTimeEntity.java

@EntityListeners(value = {AuditingEntityListener.class})
@MappedSuperclass
@Getter @Setter
public abstract class BaseTimeEntity {

   @CreatedDate
   @Column(updatable = false)
   private LocalDateTime regTime;

   @LastModifiedDate
   private LocalDateTime updateTime;

}

0개의 댓글