1. 연관관계 매핑의 기초
- 객체와 테이블 연관관계 차이를 이해
- 객체의 참조와 테이블 외래키를 매핑 => 객체 지향적 모델링은 단순히 FK 에 해당하는 변수를 사용하는 것이 아닌 객체의 참조를 FK 로 사용하는것!!
- 간단 용어 정리!!
용어 | 설명 |
방향 Direction | 단방향, 양방향 |
다중성 Multiplicity | 다대일 N:1, 일대다 1:N, 일대일 1:1, 다대다 N:M |
연관관계의 주인 Owner | 객체 양방향 연관관계 관리 |
테이블과 객체 사이의 간격!!
- 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
- 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
예제 시나리오
- 회원과 팀이 존재하며, 회원은 하나의 팀에만 소속되고, 하나의 팀에는 여러 회원이 소속될 수 있다
- 회원과 팀은 다대일의 관계를 갖는다
1) 기억하자!! @JoinColumn
@JoinColumn 에 해당하는 컬럼은 FK 가 있는 컬럼을 의미한다.
따라서 name = "FK컬럼명" 을 매핑해주면 된다
2. 단방향 연관관계
- 단방향은 말 그대로 한쪽에서 다른 한쪽으로 흐르는 일종의 시냇물 이라고 생각하면 된다 => 위에서 아래로 한쪽으로만 흐는 관계
- 이렇게 단방향 관계를 지정해두지 않으면 member_id = 1 인 teamName 을 가져오는 아주 간단한 것이라도 아래처럼 복잡한 과정을 거쳐야 한다
// 관계 설정이 없는 경우
// Member 1 이 소속된 팀 이름을 가져오기 위해서는 아래의 과정을 거쳐야 한다
// member 를 가져와서 다시 member 의 teamId 를 가져와서 다시 팀 이름을 가져오는 아주아주 복잡한 과정
Member member = em.find(Member.class, 1L);
Team team = em.find(Team.class, member.getTeamId());
String teamName = team.getTeamName();
1) 단방향 연관관계 규칙 매핑 2가지
- 이것을 이제 단방향 연관관계를 매핑을 통해 보다 쉽게 바꿀 수 있다
=> 단방향 연관관계 매핑을 아래의 2가지 규칙을 기억해서 해야한다
첫째로 중요한 것은 '어떤' 관계를 갖는지 알려주어야 한다
=> 1:1, 1:N, N:M 이고, 이때 사용하는 어노테이션이 ManyToOne, ManyToMany 등등이 있다
- 가장 중요하게 기억해야 할 부분은 어떤 관계를 갖는지 지정할때는 "현재 클래스" 를 기준으로 해야한다.
- Member(현재클래스) : Team 는 N:1 의 관계를 갖기 때문에 ManyToOne 어노테이션을 붙여주어야 한다
둘째로 중요한 것은 해당 PK 로 어떤 컬럼을 조인하는지 알려주어야 한다.
=> 이때 사용하는 어노테이션이 JoinColumn 어노테이션이고, 파라미터로 join 하는 컬럼명을 갖는다
2) 코드 만져보기
- Member
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
/*
아래처럼 단순히 PK 에 해당하는 teamId 를 사용하는 것이 아니라
Team 객체 전체가 일종의 FK 로서 사용됨 => 객체끼리 관계를 형성
private Long teamId;
*/
/*
team == PK 로 생각한다!!
이때 주의해야 할 것들이 있다.
첫째로 중요한 것은 '어떤' 관계를 갖는지 알려주어야 한다 => 1:1, 1:N, N:M 이고, 이때 사용하는 어노테이션이 ManyToOne, ManyToMany 등등이 있다
- 가장 중요하게 기억해야 할 부분은 어떤 관계를 갖는지 지정할때는 "현재 클래스" 를 기준으로 해야한다.
- Member : Team 는 N:1 의 관계를 갖기 때문에 ManyToOne 어노테이션을 붙여주어야 한다
둘째로 중요한 것은 해당 PK 로 어떤 컬럼을 조인하는지 알려주어야 한다.
- 이때 사용하는 어노테이션이 joinColumn 어노테이션이고, 파라미터로 join 하는 컬럼명을 갖는다
*/
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
private String userName;
}
- TeamName 가져오기
// 단방향 관계 설정 후에는 아래와 같이 쉽게 가져올 수 있다
// 34 번 유저가 속한 팀의 이름을 구하기 위해서는
Member member = em.find(Member.class, 34L);
// Team 과 관계설정이 되어있음으로 바로 가져올 수 있다
String teamName = member.getTeam().getTeamName();
- TeamName 변경하기
// 단방향 관계 설정 후에는 아래와 같이 쉽게 변경 가능하다
Member member = em.find(Member.class, 34L);
// teamName 을 바꿀때도 기존처럼 set 을 사용하면 된다
Team team = member.getTeam();
// 팀이름을 변경
team.setTeamName("TeammmmB");
// 변경된 team 객체를 member 에 set
member.setTeam(team);
현재의 단방향 관계 모양
3. 양방향 연관관계와 연관관계의 주인
- 단방향 연관관계를 통해서 Member 를 통해서 Team 을 조회할 수 있게 되었다.
- 그러나 아직까지도 반대인 Team 에서 Member 를 조회할 수는 없다. Why? 아직 Team 과 Member 의 연관관계가 세워지지 않았기 때문에!!
- 사실 모든 DB 의 기본은 양방향 연관관계를 기본으로 한다. 이는 DB 에는 select * from member 나 select * from team 이나 모두 가능하기 때문이다 => 이제는 이렇게 양방향으로 조회가 가능하도록 하는 양방향 연관관계 매핑을 해볼 시간이다!
- 즉 Team 에 속해있는 Member 를 찾아오기 위한 연관관계 매핑을 하는 것!!
1) Team 코드 수정
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String teamName;
/*
Member : Team 일 때는 Member 클래스를 기준으로 N:1 의 관계가 성립했다
규칙 1 ) 그렇다면 자연스럽게 Team : Member 의 관계는 현재 클래스인 Team 을 기준으로
- 1 : N 의 관계가 성립하며 이에 따라서 OneToMany 어노테이션을 사용한다.
규칙 2 ) 동시에 mappedBy 파라미터를 사용하는데 이 파라미터의 값에는
- 연관관계 주인쪽에서 현재 클래스를 지정하는 변수를 넣어준다 => Member 에서 현재 클래스 Team 을 지정하는 변수 team
- 따라서 Team 쪽에서는 Member 와 매핑되는 이름이 team 임으로 파라미터의 값을 team 으로 적는것이다
!!!! 사실 mappedBy 는 아주 어렵고, 중요한 개념으로 뒤에 다시 설명하도록 하겠다 !!!!
*/
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
2) 코드 확인
System.out.println("---------- 양뱡향 연관관계 --------------");
List<Member> list = member.getTeam().getMembers();
list.forEach(m ->{
System.out.println(m.toString());
});
4. MappedBy
- 객체와 테이블이 관계를 맺는 차이를 이해해야 비로소 MappedBy 를 이해할 수 있다
사실 양방향 연관관계라고 해도 실제로는 단방향이 2개가 있는 모양이다.
객체 연관관계 = 2개 => 팀-> 회원(단방향), 회원->팀(단방향)
그러나 테이블에서의 연관관계는 1개 뿐이다
회원 <-> 팀 (양방향)
why? teamId 라는 PK 하나로! 연결되어있기 때문
따라서! 우리도 team 과 members 둘 중 하나를 PK 로 설정해야한다
즉, 연관관계의 주인을 지정해줘야 한다
1) 연관관계의 주인 Owner
- 양방향 매핑 규칙
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정
- 연관관계의 주인만이 외래 키를 관리 ( 등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능!!
- 주인은 mappedBy 속성 사용 X => 주인이 아니면 mappedBy 로 주인을 지정해주어야 함!!!
2) 그래서 누구를 주인으로?
DB 테이블에서 외래 키 FK 가 있는 곳을 주인으로 정하자
- 아래 그림으로 봤을 때 TEAM_ID 가 PK 로 있는 쪽은 TEAM 테이블이고, TEAM_ID 가 FK 로 있는 쪽은 MEMBER 이다.
따라서 FK 있는 쪽이 주인임으로 연관관계의 주인은 MEMBER 클래스가 된다
=> 보통 연관관계 1:N 에서 N 인쪽이 주인으로 지정된다
3) 연관관계의 주인 주의점
- 연관관계의 주인쪽에서 FK 를 업데이트 하는 것이 아닌 가짜 매핑 즉 주인의 반대편에서 FK 를 업데이트 하면 제대로 DB 에 반영되지 않는다!!!
/* 옳지 못한 양방향 연관관계 매핑 */
/* 연관관계 주인이 아닌 쪽을 통해서 FK 를 수정하려고 할 때 */
// Member 생성
Member member = Member.builder()
.userName("memberBB")
.build();
em.persist(member);
// Team 생성
Team team = new Team();
team.setTeamName("TeamBB");
// Team 에 있는 members 에 member 객체 넣기
team.getMembers().add(member);
em.persist(team);
- 따라서 FK 를 업데이트 - 등록, 수정 - 하려고 한다면, 연관관계의 주인쪽에서 FK 를 다루고 활용해야 한다!
/* 옮게 된 양방향 연관관계 매핑 */
/* 연관관계 주인쪽에서 FK 를 등록 */
// Team 생성
Team team = new Team();
team.setTeamName("TeamCC");
// Member 생성
Member member = Member.builder()
.userName("memberCC")
// 연관관계의 주인이 되는 Member 쪽에서 team 을 넣어서 등록한다다
.team(team)
.build();
em.persist(member);
em.persist(team);
4) 양방향 연관관계 매핑 시 양방향으로 세팅해야 한다!
예를 들어 em.persist 만 하고 아직 commit 이전 상태라면, 혹은 EntityManager 가 clear 된 상태라면 이렇게만 사용해서는 문제가 생긴다. 이는 Member 쪽에는 team 이 세팅되어있으나 team 의 members 즉 List<member> 에는 아직 member 가 add 되지 않았기 때문이다
// 만약 이 상태에서 바로 teamDD 의 members 를 가져온다면 어떻게 될까?
Team teamDD = em.find(Team.class, team.getId());
System.out.println("---- 출력 -----");
teamDD.getMembers().forEach(m -> {
System.out.println(m.toString());
});
System.out.println("=========================");
따라서 아래처럼 2중으로, 양방향으로 세팅해줘야 한다
/* 완전 옮게 된 양방향 연관관계 매핑 */
/* 연관관계 주인쪽에서 FK 를 등록 && 주인이 아닌쪽에서 FK 관련해서 세팅 */
// Team 생성
Team team = new Team();
team.setTeamName("TeamDD");
em.persist(team);
/* team 과 member 모두 2중으로 세팅한다 */
// Member 생성
Member member = Member.builder()
.userName("memberCC")
// member -> team : 연관관계의 주인이 되는 Member 쪽에서 team 을 넣어서 등록한다
.team(team)
.build();
em.persist(member);
// team -> member : Team 에 있는 members 에 member 객체 넣기
team.getMembers().add(member);
// 만약 이 상태에서 바로 teamDD 의 members 를 가져온다면 어떻게 될까?
Team teamDD = em.find(Team.class, team.getId());
System.out.println("---- 출력 -----");
teamDD.getMembers().forEach(m -> {
System.out.println(m.toString());
});
System.out.println("=========================");
5) 양방향 연관관계 최종 정리
1. 순수 객체 상태를 고려해서 양쪽에 값을 설정하자
2. 연관관계 편의 메서드를 생성하자
3. 양방향 매핑 시 무한 루프에 주의하자 : Ex) ToString(), lombok, JSON 생성 라이브러리
=> 컨트롤러에서는 Entity 를 절대!! 반환하지 말 것, 가능한 DTO 로 변환해서 반환할 것
4. 단방향 매핑 만으로도 이미 연관관계 매핑 설계를 완료해야 함
=> 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
5. JPQL 에서 역방향으로 탐색할 일이 많음
6. 단방향 매핑을 잘하고 양방향 매핑은 필요할 때 추가해도 됨 => 테이블 영향을 주지 않음
7. 연관관계 주인의 기준은 비즈니스 로직을 기준으로 하는 것이 아닌 외래 키 FK 의 위치를 기준으로 정하자
연관관계 편의 메서드
일반적인 setter 메서드를 살짝 변형해서 1번에 해당하는 연관관계 주인과 아닌 쪽 모두의 값을 설정 할 때 보다 쉽게 하기위한 메서드
// 연관관계 매핑을 위한 메서드
// 이것을 통해서 team 을 넣어주면 해당 team 의 members 에 자기자신 객체를 넣는다
// 이렇게 하면 뒤에서 team.getMembers().add(member) 따로 setter(Team team) 해서 2번 할 필요는 없어진다
public void addMembers(Team team){
// this 는 현재 자기 자신 객체 의미한다
team.getMembers().add(this);
}
---------------------------------------
// 메서드가 동작하면서 Member 안에 team 에 매개변수로 던지는 team 을 세팅하고
// 다시 team 의 members 에는 현재 member 자기 자기 객체를 추가한다
member.addMembers(team);
5. 실전 예제 코드로 만들기
- Member, Order ,OrderItem, Item 클래스가 있을 때 아래처럼 구성하려고 한다.
- 각 클래스 연관관계에 맞춰 매핑을 시작한다
1) Memeber
@Entity(name = "ShopMember")
@Table
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Member {
@Id
@GeneratedValue // 전략 생략하면 AUTO
@Column(name="MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
// 양방향 연관관계 매핑 => 현재클래스 : member
// mappedBy 에는 연관관계의 주인쪽에서 현재 클래스를 부를 변수명
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
2) Order
package com.use.jpabasic.practice.domain;
import lombok.*;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "ORDERS")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
// 현재 클래스 : MEMBER => 하나의 MEMBER 에 여러 ORDER 이 올 수 있음 -> 1:N
@ManyToOne
@JoinColumn(name="MEMBER_ID")
private Member member;
// 양방향 연관관계 => 현재클래스 : OrderItem = 1: N
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
private LocalDateTime orderDate;
// 주문 상태는 Enum 으로
@Enumerated(EnumType.STRING)
private OrderStatus status;
// 양방향 연관관계 편의 메서드
public void addOrderItems(OrderItem orderItem){
orderItems.add(orderItem);
orderItem.setOrder(this);
}
}
3) Item
@Entity
@Table
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
}
4) OrderItem
@Entity
@Table
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public class OrderItem {
@Id
@GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
// 현재 클래스 : ORDER => 하나의 ORDERITEM 에 여러 ORDER 이 올 수 있음 -> 1:N
@ManyToOne
@JoinColumn(name = "ORDER_ID")
private Order order;
@ManyToOne
@JoinColumn(name = "ITEM_ID")
private Item item;
private int orderPrice;
private int count;
}
'Java - SpringJPA' 카테고리의 다른 글
Spring JPA (7) - JPA 고급 매핑 : 상속관계 매핑, @mappedsuperclass (0) | 2022.10.04 |
---|---|
Spring JPA (6) - JPA 개념 잡기 : 엔티티 연관관계 매핑 1:N, N:1, N:M, 1:1 (1) | 2022.10.03 |
Spring JPA (4) - JPA 개념 잡기 : 객체-테이블 매핑, 필드와 칼럼, 기본키 매핑 (0) | 2022.10.02 |
Spring JPA (3) - JPA 개념 잡기 : 영속성 컨텍스트, 플러시, 준영속 상태 (0) | 2022.10.01 |
Spring JPA (2) - Entity 객체, JPA 로 DB 연결하기, 기본 문법 사용하기 (0) | 2022.09.29 |
댓글