자바 ORM 표준 JPA 프로그래밍 - 기본편

다양한 연관관계 매핑 - 일대다

일대다 단방향

20240424132129

  • 실무에서 연관관계의 주인이 Team이 되는 경우를 말한다.
  • 대충 상황적으로 Team 입장에서 Member를 기록해야 하나, Member는 Team을 고려하지 않는 경우 나올 수 있는 설계다.
  • 그런데 실질적인 테이블 데이터 간의 연관관계를 생각해보면, Team 은 외래키를 가질 수가 없다.
  • 여기서 이 관계의 핵심문제는 개발자 입장에서 일 쪽에 작업을 한것 뿐이라고 생각했는데, 관계 특성 상 다(N)에 해당하는 쪽에도 영향을 미쳤다는 점이다.

일대다 단방향 정리

  • 일대다 단방향은 일대다에서 일(1)이 연관관계의 주인이 되는 것
  • 테이블 일대다 관계는 항상 다(N) 쪽에 외래키가 있다.
  • 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조
  • @JoinColumn 을 꼭 사용해야함. 그렇지 않으면 조인 테이블 방식을 사용해야 한다. (중간 테이블을 하나 추가해야한다.)
  • 일대다 단방향 매핑의 단점
    • 엔티티가 관리하는 외래키가 다른 테이블에 있음
    • 연관관계 관리를 위해 추가로 일(1) 쪽 UPDATE SQL 이 실행됨
  • 따라서 애매한 부분이 있으므로, 객체지향적으로 손해를 보는 부분이 있음에도 다대일 양방향으로 매핑을 거는 게 낫다.

일대다 양방향

20240424133520

  • Member 즉, 다에 해당하는 부분에 다음 어노테이션을 넣어줌으로써 해당 관계의 원칙만 지키는 형태를 만들어내는 것이다.
  • Member에 넣을 어노테이션
    • ManyToOne
    • @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) : 읽기 전용으로 만들어버리는 것

일대다 양방향 정리

  • 공식적으로 이러한 방식의 매핑은 없음
  • 위에서 언급한 방식으로 읽기 전용 필드로 만들어 양방향처럼 사용할 수 있음
  • 걍 다대일 양방향을 사용하자…

다양한 연관관계 매핑 - 일대일

일대일 : 단방향

  • 일대일 관계는 반대도 동일한 일대일이다.
  • 주 테이블이나 대상 테이블 중 외래키의 위치는 선택 가능하다.
  • 외래키에 데이터 베이스 유니크(UNI) 제약 조건 추가

20240424134927

  • 멤버와 Locker 객체를 활용해서 만들어보는 형태
  • 다대일 단방향 매핑과 유사하다.

일대일 : 주 테이블에 외래 키 양방향

20240424143149

  • 다대일 양방향 매핑처럼 외래 키가 있는 곳이 연관관계의 주인
  • 반대편은 mappedBy 적용하면 된다.

일대일 : 대상 테이블에 외래 키 단방향

20240424143324

  • 이런 방식은 지원도 안되고, 특성을 구현하여 실질적으로 존재하도록 만드는 방법도 없다.
  • 양방향 관계는 지원된다. (대상 테이블이 사실상 주인이란 소리임)

알아두면 좋은 것

  • 대상 테이블에 외래 키 양방향으로 구성하는 것이 좋은 경우가 있다.
    • 왜냐하면 대상 즉, Locker 가 여러 개를 복수로 사용하게 되는 비즈니스 로직으로 변경된다고 하자. 이때 연관관계의 주인이 Member 인 경우, 수정해야할 포인트들이 많이 생긴다.
    • 하지만 역으로 대상 테이블에 외래 키를 넣게 되면, 유니크 설정만 풀고, Member는 읽기 구조로 다대일로 변경하면 매우 손쉽게 비즈니스 로직 수정에 대응이 가능하다.
  • 그러나 여기서 하나의 Locker 를 여러 사람이 소유하고 사용할 수 있는 비즈니스 로직으로 된다고 하면, 그때는 주 테이블에 외래 키를 넣는 구조가 되는 게 수정이 쉽다.
  • 즉 JPA, 객체를 활용한 데이터를 다룰 때는 종합적인 판단이 중요하다.
  • 강의 저자는 명확한 1:1 관계라고 한다면 주 테이블 양방향 내지는 단방향으로 가져간다고 한다.

일대일 정리

  • 주 테이블에 외래 키를 놓는 경우
    • 주 객체가 대상 객체의 참조를 가지는 것처럼, 주 테이블에 외래 키를 두고 대상 테이블을 찾음
    • 객체 지향 개발자 선호
    • JPA 매핑이 편리함
    • 장점 : 주 테이블 조회로 대상까지 쉽게 확인 가능
    • 단점 : 값이 없으면 외래 키에 null 허용
  • 대상 테이블에 외래 키를 놓는 경우
    • 대상 테이블에 외래 키가 존재
    • 전통적인 데이터베이스 개발자 선호
    • 장점 : 주 테이블과 대상 테이블을 일대일에서 일대 다로 관계 변경 시 테이블 구조 유지
    • 단점 : 프록시 기능의 한계로 지연로딩으로 설정해도 항상 즉시 로딩

다양한 연관관계 매핑 - 다대다

다대다

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
  • 연결 테이블을 추가해서 일대다-다대일 관계로 풀어 내야 한다.

20240424145251

  • 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계를 표현 할 수 있다(이부분이 애매한 문)

20240424145419

  • @ManyToMany 사용
  • @JoinTable 로 연결 테이블 지정
  • 다대다 매핑 : 단, 양방향 모두 가능

다대다 매핑의 한계

  • 편리해 보이지만 실무에서 사용되기가 말이 안됨
  • 연결 테이블이 단순히 연결만 하고 끝나지 않음
    • 추가적인 데이터가 더 들어가야 하는 경우 있음
    • 숨겨진 테이블로 인해 쿼리가 더 많이, 더 이상하게 날아갈 수 있음
  • 주문 시간, 수량 같은 데이터 들어올 수 있음

20240424150050

다대다 한계 극복

  • 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격 시켜준다)
  • @ManyToMany -> @OneToMany & @ManyToOne

20240424150336

다양한 연관관계 매핑 - 실전예제 3

20240424151914 20240424152047 20240424152000

  • 해당 내용을 전체 구현을 다하더라도, 최신 JPA 예약어에 걸리는 것인지 ORDER 테이블이 에러가 발생한다.
  • 이럴 때는 백틱으로 감싸고 쌍따옴표로 테이블 명임을 지정해줘야 에러가 발생하지 않았다 … ㅠ
// Member.java
package practice_3;  
  
import jakarta.persistence.*;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
public class Member {  
  
    @Id  
    @GeneratedValue    @Column(name = "MEMBER_ID")  
    private Long id;  
  
    @Column  
    private String name;  
  
    @Column  
    private String city;  
  
    @Column  
    private String street;  
  
    @Column  
    private String zipcode;  
  
    @OneToMany(mappedBy = "member")  
    private List<Order> orders = new ArrayList<Order>();  
  
}
// Order.java
package practice_3;  
  
import jakarta.persistence.*;  
  
import java.time.LocalDate;  
import java.time.LocalDateTime;  
import java.util.ArrayList;  
import java.util.Date;  
import java.util.List;  
  
@Entity  
@Table(name = "`ORDER`")  
public class Order {  
    @Id  
    @GeneratedValue    @Column(name = "ORDER_ID")  
    private Long id;  
  
    @ManyToOne  
    @JoinColumn(name = "MEMBER_ID")  
    private Member member;  
  
    @OneToMany(mappedBy = "order")  
    private List<OrderItem> orderItems = new ArrayList<>();  
  
    @OneToOne  
    @JoinColumn(name = "DELIVERY_ID")  
    private Delivery delivery;  
  
    @Column  
    private Date orderDate;  
  
    @Enumerated(EnumType.STRING)  
    private OrderStatus status;  
  
}
// Delivery.java
package practice_3;  
  
import jakarta.persistence.*;  
  
@Entity  
public class Delivery {  
  
    @Id  
    @GeneratedValue    @Column(name = "DELIVERY_ID")  
    private Long id;  
  
    // 이렇게 잡음으로써 Order가 주인, Delivery가 양방향의 읽기 전용이 된다.  
    @OneToOne(mappedBy = "delivery")  
    private Order order;  
  
    @Column  
    private String city;  
  
    @Column  
    private String street;  
  
    @Column  
    private String zipcode;  
  
    @Enumerated(EnumType.STRING)  
    private DeliveryStatus status;  
  
}
// OrderItem.java
package practice_3;  
  
import jakarta.persistence.*;  
  
@Entity  
public class OrderItem {  
    @Id  
    @GeneratedValue    @Column(name = "ORDER_ITEM_ID")  
    private Long id;  
  
    @ManyToOne  
    @JoinColumn(name = "ORDER_ID")  
    private Order order;  
  
    @ManyToOne  
    @JoinColumn(name = "ITEM_ID")  
    private Item item;  
  
    @Column  
    private int orderPrice;  
  
    @Column  
    private int count;  
  
}
// Item.java
package practice_3;  
  
import jakarta.persistence.*;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
public class Item {  
    @Id  
    @GeneratedValue    @Column(name = "ITEM_ID")  
    private Long id;  
  
    @Column  
    private String name;  
  
    @Column  
    private int price;  
  
    @Column  
    private int stockQuantity;  
  
    @ManyToMany(mappedBy = "items")  
    private List<Category> categories = new ArrayList<>();  
}
// Category.java
package practice_3;  
  
import jakarta.persistence.*;  
  
import java.util.ArrayList;  
import java.util.List;  
  
@Entity  
public class Category {  
    @Id  
    @GeneratedValue    @Column(name = "CATEGORY_ID")  
    private Long id;  
  
    @Column  
    private String name;  
  
    @ManyToMany  
    @JoinTable(name = "CATEGORY_ITEM",  
        joinColumns = @JoinColumn(name = "CATEGORY_ID"),  
            inverseJoinColumns = @JoinColumn(name= "ITEM_ID")  
    )  
    private List<Item> items = new ArrayList<>();  
  
    @ManyToOne  
    @JoinColumn(name = "PARENT_ID")  
    private Category parent;  
  
    @OneToMany(mappedBy = "parent")  
    private List<Category> children = new ArrayList<>();  
  
  
}
  • 구현에 포함되지만 enum 같은 것들은 제외하였다.
  • ManyToMany 는 확실히 구현 방식에 비해 사용성이 떨어지는게 느껴진다. 가능하면 OrderItem 처럼 쪼개서 활용하는게 낫다고 보여진다.