[김영한 - 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;




© 2020.12. by 따라쟁이

Powered by philz