[김영한 - JPA 기본] #6 다양한 연관관계 매핑
연관관계 매핑시 고려사항들
- 다중성
- 일대다, 등
- 단방향, 양방향
- 양방향시 연관관계의 주인 고려
단방향, 양방향
테이블
- 외래 키 하나로 양쪽 조인 가능
- 방향이라는 개념이 없다 (아래 참고)
SELECT M.*
FROM MEMBER M
JOIN TEAM T
ON M.MEMBER_ID = T.MEMBER_ID;
SELECT *
FROM TEAM T
JOIN MEMBER M
ON T.MEMBER_ID = M.MEMBER_ID;
객체
- 레퍼런스(참조) 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향(사실은 단방향 2개)
연관관계의 주인
양방향 관계는 참조가 2군데 있는데 둘중 테이블의 외래 키를 관리할 곳을 지정해야 함
- 그래서 주인은 외래키를 관리하는 참조 필드
insert/update
가능
주인의 반대편: 외래 키에 영향을 주지 않음
read-only
다대일 [N:1] @ManyToOne
다
가 연관관계의 주인- 반대 방향은 일대다
가장 많이 사용하는 연관관계
@Entity
public class Member {
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
만일 반대쪽 방향에도 연관관계를 걸어서 양방향으로 만들고 싶다면?
@Entity
public class Team {
@OneToMany(mappedBy = "team")
private Member member;
}
일대다 [1:N] @OneToMany
- 맨 앞 글자의
일
이 연관관계의 주인 - 권장하지 않지만 표준스펙에서 지원하므로 강의 내용에 있다
@JoinColumn
을 꼭 사용해야 한다
-그렇지 않으면 default로 조인 테이블 전략을 사용함 (중간에 테이블을 하나 추가함) (매핑 테이블)
public 접근제어자 기본적인 어노테이션 등은 이하 생략합니다 !
class Team {
...
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
실행부
tx.begin();
Member member = new Member("user A");
em.persist(member);
Team team = new Team("H.R.");
em.persist(team);
team.getMembers().add(member);
실행 쿼리
Hibernate:
/* insert hellojpa.domain.Member
*/ insert
into
Member
(age, createDate, description, lastModifiedDate, name, roleType, id)
values
(?, ?, ?, ?, ?, ?, ?)
Hibernate:
/* insert hellojpa.domain.Team
*/ insert
into
Team
(name, TEAM_ID)
values
(?, ?)
그리고.. 아래가 중요하다 !
Hibernate:
/* create one-to-many row hellojpa.domain.Team.members */ update
Member
set
TEAM_ID=?
where
id=?
위 실행부를 보면 JPA를 아직 잘 모르는 사람에게 혼동을 줄 수 있다
분명 Update는 team에 했는데 정작 변경은 Member테이블에 되니까 ..
장점보다 단점이 많은 방식이다
단점 정리
- 엔티티(
Team
)가 관리하는 외래 키가 다른 테이블(MEMBER
)에 있음 - 연관 관계 관계를 위해 추가적인 UPDATE 쿼리 발생
결론
- 일대다 보다는 다대일 양방향이 낫다.. 더 나은건 다대일 단방향
충격과 공포의 양방향 매핑.. !
- 놀랍게도 일대다도 양방향이 가능하다 ! 아래 코드를 보자 !
class Member {
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
}
위 처럼 해주면 된다 !
insertable
, updatable
옵션을 false로 한 이유는 이미 상대편에(Team) 연관관계 주인으로 true로 되어 있기 떄문이다
한쪽에만 write 권한이 있다는 걸 보여주어야 하는 것 같다 둘다 쓰기 권한을 가지면 꼬일 수도 있으니..
문제는 저 옵션을 끄더라도 예외가 발생하지 않는다 ! ㅠㅠ
일대일 [1:1] @OneToOne
말 그대로 주 테이블과 대상 테이블 모두 유일키 제약 조건을 갖는 상황에서의 매핑이다
상황은 Mmeber와 Locker 객체로 가정한다
- Member가 주 객체, MEMBER가 주 테이블
- Locker가 주 객체, LOCKER가 주 테이블
- 주 테이블에 PK있을 경우 (MEMBER 테이블)
- 대상 테이블에 PK가 있을 경우
- JPA에서 지원하지 않는다
단방향 매핑
자세한 구문은 생략하였습니다
class Member {
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
양방향 매핑
- 반대쪽 테이블
class Locker {
@OneToOne(mappedBy = "locker")
private Member member;
주 테이블(MEMBER)에 외래 키
- MEMBER는 많이 조회하며 조인의 대상이 되는 테이블
- 객체 지향 개발에 좋음
- 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능 - Member를 통해서 Locker를 쉽게 알 수 있음
- 단점: 값이 없을 경우 외래 키(LOCKER_ID)에 null을 허용해야 한다 (유일키 제약을 못 쓴다)
대상 테이블(TEAM)에 외래 키
- 전통적인 RDB 개발에 선호
- 장점: Locker등 대상 테이블을 일대일 -> 일대다로 변경할 떄 편하다
- 예를들어 한 회원이 여러 개의 Locker를 갖을 때
- 단점: 프록시 기능 한계로 지연로딩으로 설정해도 항상 즉시 로딩
- Member를 조회했는데 Locer가 같이 조회됨
다대다 [N:N] @ManyToMany
RDB는 정규화된 테이블 2개로 N:N 관계를 정식적으로 표현할 수 없다
연결(조인) 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다
엔티티 코드
- 단방향
class Member {
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
- 양방향일 경우
class Product {
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
실무에서 사용하지 않는다 !
- 실무에선 보통 연결 테이블이 단순히 연결만 하고 끝나지 않음
- 연결 테이블에 주문시간, 수량 같은 데이터가 들어올 수 있음
- 중간 테이블에서 생각하지 못한 쿼리가 나갈 수도 있음
다대다 한계 극복
- 연결 테이블용 엔티티 추가 (엔티티로 승격)
@ManyToMany
->@OneToMany
,@ManyToOne
엔티티로 승격된 연결테이블 작성 코드
@Entity(name = "MEMBER_PRODUCT")
public class MemberProduct { // 실제 업무에선 Orders 등의 이름으로 의미있는 네이밍을 쓰자 !
@Id @GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member members;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product products;
@Column(name = "ORDER_COUNT")
private int orderCount;
private LocalDateTime orderDateTime;
class Member {
@OneToMany(mappedBy = "members")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
class Product {
@OneToMany(mappedBy = "products")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
키에 대한 조언
MemberProduct의 테이블의 키에 대한 설명이다
이때 MemberProduct의 테이블명은 ORDERS 라 가정
MEMBER_ID, PRODUCT_ID로 PK를 나타낼 수 있을지라도
ORDER_ID 등으로 PK를 표현하는 것이 좋다
위와 같은 특정 테이블을 의존하는 복합키를 사용하며 구조를 변경하거나 할 때 유연성이 떨어진다
- ID가 다른 테이블에 의존하는 것 자체가 유연성이 떨어며… 시스템을 갈아치기 쉽지가 않음
비즈니스적으로 의미가 없는 ID를 일관성있게 @GeneratedValue
를 달아주자
기타 알게된 사항들
- 실무에서는 Entitiy 객체에
setter
를 거의 쓰지 않는다- 생성자에서 모두 완성을 시키거나
- 빌더 패턴을 사용한다
조인하려는 상대 테이블의 컬럼이 PK가 아닌 경우
public class Member implements Serializable {
@Column(name = "MEMBER_ID2", unique = true)
private Long id2;
}
@Entity(name = "ORDERS")
public class Order {
@JoinColumn(name = "MEMBER_ID2", referencedColumnName = "MEMBER_ID2")
private Member member;
}
https://docs.jboss.org/hibernate/jpa/2.1/api/javax/persistence/JoinColumn.html
https://velog.io/@kir3i/JPA-PK%EA%B0%80-%EC%95%84%EB%8B%8C-%ED%95%84%EB%93%9C%EB%A5%BC-%EC%B0%B8%EC%A1%B0%ED%95%98%EB%8A%94-FK%EB%A5%BC-%EB%A7%8C%EB%93%A4-%EB%95%8C
실전 예제 코드
일부만 가져왔으며 몇몇 부분에 대해선 생략하였습니다
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(length = 30)
private String name;
private String city;
@Column(name = "ZIP_CODE")
private String zipcode;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
@Entity(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
@Entity
public class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
@Column(name = "ITEM_NAME")
private String name;
@OneToMany(mappedBy = "item")
private List<OrderItem> orderItem = new ArrayList<>();
@Entity
public class OrderItem {
@Id
@GeneratedValue
private Long id;
@ManyToOne
@JoinColumn(name = "ORDER_ID")
private Order order;
@ManyToOne
@JoinColumn(name = "ITEM_ID")
private Item item;
@Column(name = "ORDER_PRICE")
private int orderPrice;