ORM 표준 JPA 프로그래밍 - 다양한 연관관계 매핑

링딩·2022년 9월 4일
0

ORM 표준 JPA

목록 보기
5/6

단방향, 양방향

테이블은 외래 키 하나로 양쪽을 조인할 수 있다.
-> 방향이란 개념이 x

객체는 참조용 필드가 있는 쪽으로 조인이 가능하다.
-> 한쪽만 참조하면 단방향
-> 사실 양방향은 없는데 서로 참조하면 양방향이라고 한다.


연관관계 주인

• 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺음
• 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데
-> 테이블은 하나임에 불구하고
객체 양방향 관계는 참조가 2군데 있음. 이 참조 2군데 중 '테이블의 외래 키(FK)'를 관리할 곳을 지정해야함
연관관계의 주인: 외래 키를 관리하는 참조
주인의 반대편: 외래 키에 영향을 주지 않음, 단순 조회만 가능


1. 다대일

다대일 단방향

  • 가장 많이 사용함
  • 다대일의 반대는 일대다
  • @JoinColumn은 있으나 반대편에 연관관계 주인이 없다
    -> 양방향이 아니니까

다대일 양방향


• 외래 키가 있는 쪽이 연관관계의 주인
• 양쪽을 서로 참조하도록 개발
• 다대일 단방향에서 상대 클래스에 @방향(mappedby="")로 연관관계가 추가되었다고 생각하면 쉬움


2.일대다

일대다 단방향

• Team은 Member를 아는데 Member는 Team을 알고 싶지 않은 상황이다.
• DB입장에선 무조건 다(Member)쪽에 외래키가 들어가게 된다.
-> DB설계상 절대적이다.
• 그러면 Team의 List 값을 바꼈을 때 다른 테이블인 Member 테이블에 업데이트를 해줘야 한다.


Team 코드

@Entity
public class Team {

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

    private String name;

    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();
    ....
}
  • 단방향으로 Team에서만 @JoinColumn 을 해준ㄷ.

Member 코드

@Entity
public class Member {

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

    @Column(name = "USERNAME")
    private String username;

    ...
}

[UPDATE 쿼리의 발생]

Member member = new Member();
            member.setUsername("member1");
            em.persist(member);

            Team team = new Team();
            team.setName("teamA");
            team.getMembers().add(member); // update 쿼리의 발생

            em.persist(team);

UPDATE 쿼리가 추가적으로 발생한다..(이전 다대일 때랑 달리)

  • 이게 문제가 된다면 실무에서 많은 테이블이 엮여서 돌아갈 때 이런 식으로 추가 쿼리가 발생된다면,,,, 문제가 발생할 것
    => 그러니 차라리 다대일 단뱡향을 쓰고 필요할 때만 '양방향'을 추가하자

일대다 단방향 정리

• 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
-> Q. FK 있는 쪽이 연관관계 주인 아님?

상황: 일대다는 객체 기준에서는 다에 해당하는 멤버는 일에 해당하는 팀을 모르고 있습니다. 그러나 일에 해당 하는 팀은 자신에게 속한 모든 멤버들(List members)을 알고 있고요.

  • 문제
    다에 해당하는 Member 테이블에는 외래키가 존재하는 반면에, Member 객체에는 Team에 대한 정보가 존재하지 않은 것입니다.
    => (결과): 어쩔 수 없이 '1'에 해당하는 Team(멤버의 정보를 들고 있는)이 외래키를 관리하는 연관관계의 주인이 되는 것입니다.
  • 참조 인프런

• 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음
• 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하는 특이한 구조
• @JoinColumn을 꼭 사용해야 함.
-> 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)


[일대다 단방향 매핑 단점]

• 엔티티가 관리하는 외래 키가 다른 테이블에 있음
• 연관관계 관리를 위해 추가로 UPDATE SQL 실행

결론: 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자



일대다 양방향

• 이런 매핑은 공식적으로 존재X (좀 억지스럽긴 하지만..)
• Member에서도 Team이 보고 싶어(야매로 가능함)
-> 역방향이 ok 되는 상황
@JoinColumn(insertable=false, updatable=false)
-> 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법
• 결론 : 다대일 양방향을 사용하자

Member 클래스와 Team 클래스 코드

public class Member{
  
  @ManyToOne
  @JoinColumn(name = "team_id", insertable = false, updatable = false)
  private Team team;
  }
  ...
}

//ㅆ=Team 클래스
public class Team {
  
  @OneToMany
  @JoinColumn(name = "TEAM_ID")
  private List<Member> members = new ArrayList<>();
  }

근데 왜 Member 클래스에 @JoinColumn(name = "")이 "MEMBER_ID"가 아닌 "TEAM_ID"인가

  • 이 말씀이 이해에 도움을 좀 주었다.


3.일대일 [1:1]

일대일 관계

  • 일대일 관계는 그 반대도 일대일
  • 주 테이블이나, 대상 테이블 중에 외래 키 선택 가능
    - 주 테이블에 외래 키
    - 대상 테이블에 외래 키
  • 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가

일대일: 3-1) 주 테이블에 외래 키 단방향

여기서는 Member를 주테이블로 생각함.

Member 클래스

public class Member {

    @OneToOne
    @JoinColumn(name = "LOCKER_ID")
    private Locker locker;

    ...
}
  • @JoinColumn 써준다.
  • 다대일(@ManyToOne) 단방향 매핑과 유사

일대일: 3-1) 주 테이블에 외래 키 '양'방향

public class Locker {
    
  @OneToOne(mappedBy = "locker")
    private Member member;
  ...
}
  • 단방향에서 썼던 Member 클래스는 그대로
    Locker에서 mappedBy="" 를 써준다.
  • 다대일 양방향 매핑처럼 외래 키가 있는 곳연관관계의 주인



일대일: 3-2) 대상 테이블에 외래 키 '단'방향 => 지원 x

  • 마치 일대다 단방향 때 같은데 반대편 Locer 테이블의 MEMBER_ID는 관리 할 수도 없고, 지원도 안되고 방법도 없다.
  • 양방향은 된다고 함.

일대일: 3-2) 대상 테이블에 외래 키 '양'방향 => 지원 ㅇ

  • 양방향이 되었으므로 Locker의 Member 필드를 '연관관계의 주인'으로 삼으면 된다.
  • '일대일 주 테이블'에 외래 키 양방향과 매핑 방법은 같음.



[최종] 일대일의 정리

주 테이블에 외래 키

  • 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
  • 객체지향 개발자 선호
  • JPA 매핑 편리

(장점): 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
(단점): 값이 없으면 외래 키에 null 허용

대상 테이블에 외래 키

  • 대상 테이블에 외래 키가 존재
  • 전통적인 데이터베이스 개발자 선호

(장점): 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
(단점): 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩
-> 여기서 말하는 프록시는 뒤에서 보기로 하자

이 프록시...(뒤에서 더 자세히 다룰 것임)

Member에 locker를 조회했다고 하면, JPA 입장에서는 locker가 값이 있는지 없는지는 Locker까지 뒤져서 봐야 값이 있는지 알 수 있다.
=> 값이 있어야만 locker가 있다는 것을 알 수 있는거임.
=> 지연로딩을 하는게 아무 의미가 없음. 그저 쿼리가 하나 나가는거임
(이해가 어려운 부분이였다 이 부분은 다시 한 번 더 보자...)



다대다[N:M]

  • 실무에서 안쓰는걸 추천하다.
  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
  • 연결(중간) 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함


객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능하다.


Member, Product 코드에서 사용

public class Member {

   
    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT")
    private List<Product> products = new ArrayList<>();
    ...
}

public class Product {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
  	//getter,setter...
  }
  • @ManyToMany 사용
  • @JoinTable연결 테이블 지정
  • 다대다 매핑: 단방향, 양방향 가능

만약 '양방향'으로 변경한다고 하면 Product에도 참조를 추가해준다.

public class Product {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
  
	@ManyToMany(mappedBy = "products")
    private List<Member> members = new ArrayList<>();
  }

다대다 매핑의 한계(중요!)

편리한 것 같지만 실무에서 사용하지 않는다.

1 매핑정보만 딱 들어가고, 중간테이블에 추가적인 데이터를 넣을 수 없음

  • 연결 테이블이 단순히 연결만 하고 끝나지 않음
  • 주문시간, 수량 같은 데이터가 들어올 수 있는데 이런 데이터들을 쓸 수가 없음.

2 내가 생각하지 못한 쿼리가 이상하게 나간다(중간 테이블이 숨겨져 있기에)


다대다 한계 극복(중요!)

  • 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
  • @ManyToMany -> @OneToMany , @ManyToOne

위의 그림을 더 추천합니다.

중간 엔티티가 PK,FK를 그대로 가지는 것 보다 오히려 PK를 따로 만들어주는 것이 더 낫다. => 유연성 때문에

PK도 더 유연하게 쓸 수 있고, 필요하면 그 때 DB에 제약조건을 쓰면 되는 것이고, JPA에서 두 개의 PK를 묶어서 만들면 새로운 composeId를 만들어야 하는 귀찮음을 감수해야 함..

또한 DB 설계에서도 장단점이 있다.

그러나 강사님은 모든 엔티티에 GeneratedValue로 PK를 만드는 식으로 개발한다.

=> id 라는게 어디에 종속되는 식으로 걸리게 되면, 시스템이 유연하게 유지보수 하는 것이 어렵다. 곧 비즈니스적으로 의미가 없는 값들이 들어갈 수도 있고 중간 수정이 되어서 PK가 추가가 되어야 한다는 식으로 PK,FK로 들어가는 것보다 훨씬 낫다.





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

배송, 카테고리 추가 - 엔티티

• 주문과 배송은 1:1(@OneToOne)
• 상품과 카테고리는 N:M(@ManyToMany)


배송, 카테고리 추가 - ERD


배송, 카테고리 추가 - 엔티티 상세

Item과 Category 코드

@Entity
public class Item {

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

    private String name;

    private int price;
    private int stockQuantity;

    @ManyToMany(mappedBy = "items")
    private List<Category> categories = new ArrayList<>();
  ...
 }
  

@Entity
public class Category {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne //자식 입장에서
    @JoinColumn(name = "PARENT_ID")
    private Category parent; //상위

    @OneToMany(mappedBy = "parent")//parent와 양방향관계
    private List<Category> child = new ArrayList<>();

    @ManyToMany
    @JoinTable(name = "CATEGORY_ITEM",
                joinColumns = @JoinColumn(name = "CATEGORY_ID"),
            inverseJoinColumns = @JoinColumn(name = "ITEM_ID")
    )
    private List<Item> items = new ArrayList<>();
}
  • 예제 적용을 위해 @ManyToMany를 쓴 것임. 실제로는 '중간 테이블'을 이용해 1:N, N:1로 나눔
    -> 실전에서는 @ManyToMany 사용해선 x

Order - Delivery 코드

@Entity
@Table(name = "ORDERS")
public class Order {

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

    @ManyToOne
    @JoinColumn(name = "MEMBER_ID") //연관관계 주
    private Member member;

    //Delivery와 일대일
    @OneToOne
    @JoinColumn(name = "DELIVERY_ID")
    private Delivery delivery;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();
...
}
  
@Entity
public class Delivery {
    @Id @GeneratedValue
    private Long id;

    private  String city;
    private String street;
    private String zipcode;
    private DeliveryStatus status;

}


@ManyToMany 관해서 정리

• 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1
• 실전에서는 중간 테이블이 단순하지 않다.
• @ManyToMany는 제약: 필드 추가X, 엔티티 테이블 불일치
• 실전에서는 @ManyToMany 사용X


@JoinColumn

• 외래 키를 매핑할 때 사용

@ManyToOne - 주요 속성

• 다대일 관계 매핑

@OneToMany - 주요 속성

• 다대일 관계 매핑

profile
초짜 백엔드 개린이

0개의 댓글