주문 정보(Order)에는 주문한 고객(Member), 배송 정보(Delivery), 주문한 상품 목록(OrderItem List)가 있다. 또한 주문 상태 (OrderStatus)와 주문 날짜(Date)도 있다.
Order과 Item은 다대다 관계이다. Order은 여러 Item을 주문할 수 있고, Item은 여러 Order에 쓰일 수 있다. 다대다 관계를 일대다, 다대일 관계로 바꿔주기 위해 Order_Item이라는 연결 테이블 을 사용한다. OrderItem이라는 도메인을 만들어 연결 엔터티로 사용한다.
OrderItem은 주문한 상품으로 Order와 Item을 갖고 있다. 또한 주문한 가격 (orderPrice)와 주문한 상품 개수(count)도 정보로 갖고 있다.
Category는 Item과 다대다 관계이다. 다대다 관계를 일대다, 다대일 관계로 바꿔주기 위한 CATEGORY_ITEM이란 연결 테이블을 생성했지만, 따로 연결 엔터티를 사용하지 않았다.
도메인에서는 Orders에서 객체인 Member와 Delivery를 갖지만, 테이블에서는 식별자인 MEMBER_ID와 DELIVERY_ID를 갖는다.
Orders와 Member는 테이블 구조에서 보다시피 다대일 관계다.
도메인 모델이 보면 Member 도메인을 Order의 객체로 가지고 있다.
private String member_id가 아니라, private Member member 필드를 가지고 있다.
Order와 Delivery는 테이블 구조에서 보다시피 일대일 관계다.
Order와 OrderItem은 일대다 관계다. 하나의 주문(Order)에 여러 주문 상품(OrderItem)이 있다.
Category와 Item은 다대다 관계다.
@JoinTable 속성
객체는 참조(주소)를 이용해 관계를 맺고, 테이블은 외래 키(FK)를 이용해 관계를 맺는다.
@OneToMany, @ManyToOne, @OneToOne 모두 객체의 참조와 테이블의 외래 키를 매핑하기 위함이다.
객체와 테이블의 차이점은, 객체는 Order 안에서 Member 객체를 참조하는 Order -> Member 단방향 관계이다. 반면, Table은 항상 양방향 관계이다.
만약, 객체가 양방향 관계를 가지고 싶다면 Order와 OrderItem 처럼 서로 객체를 참조하면 된다. Order는 List로 OrderItem을 참조하고, OrderItem은 Order를 참조하고 있다. 이렇게 객체 간 양방향 관계를 만들면 연관관계의 주인을 정해야 한다.
연관 관계 주인만이 데이터베이스 연관관계와 매핑되고, 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하고, 주인이 mappedBy를 사용하지 않는다.
즉, Order는 OrderItem 간의 관계에서 주인이 아니고, OrderItem이 주인이다.
연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다. 주인이 아닌 반대편은 읽기만 가능하고 외래키를 변경하지는 못한다.
따라서, 외래 키를 테이블 컬럼에서 관리하는 ORDER_ITEM을 연관 관계의 주인으로 정한다.
참고로, 일대다, 다대일 관계에서는 항상 다 쪽(ORDER_ITEM)이 외래 키를 가진다.
다 쪽인 @ManyToOne은 항상 연관 관계의 주인이 되므로 mappedBy를 설정할 수 없다.
즉, @ManyToOne은 mappedBy 속성이 없다.
원래라면 Order 내 OrderItem List에 OrderItem을 추가해도, 추가된 OrderItem이 DB에 반영되지 않는다. Order는 연관 관계의 주인이 아니여서 읽기 기능(Select)만 가능하기 때문이다.
하지만 Cascade 속성을 사용함으로써, OrderItem List에 OrderItem을 추가해도 DB에 반영된다.
Cascade는 영속성 전이로 연관된 엔터티도 함께 영속 상태로 만든다.
JPA에서 엔터티를 저장할 때 연관된 모든 엔터티는 영속 상태여야 한다.
영속성 전이를 사용하면 부모만 영속 상태로 만들면 연관된 자식까지 한 번에 영속 상태로 만들 수 있다.
즉, 부모인 Order 엔터티가 영속 상태가 될 때, 연관된 자식인 OrderItem들까지 한 번에 영속상태가 된다는 뜻이다. CasecadeType.All 이므로 Order가 저장되거나 삭제될 때, OrderItem까지 같이 저장되거나 삭제된다.
참고로, Cascade Type에는 All, Persist(영속), Merge(병합), Remove(삭제), Refresh, Detach 가 있다.
양방향 연관 관계를 설정하고 가장 흔히 하는 실수는 연관 관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다. Cascade가 없으면 연관 관계 주인만이 값을 저장할 수 있다. 주인이 아닌 곳에 값을 입력하면 DB에 저장되지 않는다.
객체 관점에서도 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. 안그러면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
즉, 객체의 양방향 연관 관계는 양쪽 모두 관계를 맺어줘야 한다.
Order와 OrderItem은 양방향 관계이다.
Order에서 OrderItem을 OrderItem List에 추가할 때, OrderItem에도 Order를 세팅해준다.
이렇게, addOrderItem처럼 한 번에 양방향 관계를 설정하는 메소드를 연관 관계 편의 메소드라 한다.
객체 간의 관계가 변경되면 반드시 양쪽에서 참조를 끊어줘야 한다.
JPA의 Fetch 전략에는 2가지가 있다.
JPA의 기본 페치(fetch) 전략은 연관된 엔터티가 하나면 즉시 로딩을, 컬렉션이면 지연 로딩을 사용한다. 추천 하는 방법은 모든 연관관계에 지연 로딩을 사용하는 것이다. 실제로 사용하는 상황을 보고 꼭 필요한 곳에만 즉시 로딩을 사용하도록 최적화하면 된다.
JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다.
이로써 연관된 객체를 계속 탐색할 수 있게 해주고, 이를 객체 그래프 탐색이라 한다.
Order에서 주문한 사람의 이름을 알기 위해, Order.member.getName()을 사용할 때, JPA가 Select SQL을 자동으로 실행해서 member의 정보를 DB로 부터 가지고 온다.
만약 FetchType.Lazy를 사용하지 않았다면, 즉시 로딩(EAGER)로 Order 조회 시 member도 같이 DB에서 조회되었을 것이다. 그러면 member.getName()으로 주문한 사람의 이름을 가져올 때, DB로 부터 가지고 오지 않고 이미 조회해서 영속 컨텍스트에 있는 member에서 가지고 온다.