이 장의 핵심은 객체의 연관관계와 테이블의 외래 키를 매핑하는 것이다.
본격적인 시작에 앞서 연관관계 매핑 키워드 미리 알아두기
Direction
: 단방향과 양방향이 있다.A 객체 -> B 객체
참조A 객체 -> B 객체
, B 객체 -> A 객체
참조다중성 Mulitiplicity
: N:1
, 1:1
, 1:N
, N:M
이 있다.
연관관계 주인 owner
: 외래키를 실제 관리하는 참조 필드
일단 아래와 같은 연관 관계가 있다고 가정하자.
위의 시나리오를 구현하기 위해서 Member(=회원), Team Entity를 작성해보자.
package hello.jpa;
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@Column(name = "TEAM_ID")
private Long teamId;
// GETTER, SETTER 생략
}
package hello.jpa;
import javax.persistence.*;
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
// GETTER, SETTER 생략
}
그런데 위처럼 작성한 코드는 객체를 제대로 다루는 방식일까?
객체는 다른 객체와 연관관계를 맺을 때는 참조를 사용한다.
지금처럼 Member 엔티티가 Team 엔티티의 특정 필드의 값을 갖는 것은 객체에서 말하는 연관관계라고 하기 힘들다.
그러면 한번 코드를 아래처럼 바꿔보자.
package hello.jpa;
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
private Team team; // 참조를 사용하도록 코드 변경
// GETTER, SETTER 생략
}
그럴듯하다.
하지만 생각해면 우리가 DataBase의 Table 에서 서로 연관관계를 맺을 때 외래키를 사용한다.
DB의 외래키
와 객체의 참조
라는 개념은 서로 상이한데, JPA에서는 어떤 방법으로 매핑을 할까?
방법은 아래와 같다.
package hello.jpa;
import javax.persistence.*;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne // 추가1! Member <-> Team은 N:1 관계
@JoinColumn(name = "TEAM_ID") // 추가2! 외래키의 이름
private Team team;
// GETTER, SETTER 생략
}
이렇게 하면 단방향 연관관계 매핑은 끝이다!
@ManyToOne
를 통해서 Member과 Team이 N:1
의 다중성을 갖는다@JoinColumn
을 통해서 실제 DB Table 외래키 이름을 매핑한다.이번엔 양방향 매핑을 알아보자.
앞서서는 Member ->
Team 방향으로 단방향 연관관계를 갖었다.
이번에는 Team ->
Member 방향으로도 연관관계를 갖는 코드를 짜보자.
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // 반대편 연관관계를 갖는 엔티티의 필드명
private List<Member> members = new ArrayList<>();
// GETTER, SETTER 생략
}
@OneToMany
는 이해가 되지만, mappedBy = "team"
은 대체 뭘까?
알려주기 이전에 잠시 아래와 같은 문제점을 생각해보자.
객체는 양쪽에서 연관관계를 맺을 때 위 코드처럼 서로 참조값을 갖는 변수를 갖으면 된다.
하지만 DataBase Table 에서의 하나의 외래키로 연관관계를 맺는다.
여기서 정말 애매한 점이 나온다.
앞서 단방향 연관관계에서는 객체에서 하나의 참조값 변수, 그리고 하나의 DataBase Table 외래키를 매핑하면 끝났다. 덕분에 참조값 변수 하나로 외래키를 insert, update, 즉 관리가 됐다.
그렇다면 현재 상황에서는 어떨까?
테이블의 TEAM_ID(FK) 값은 Member 객체의 team 이 수정되었을 때 update되어야 하는가?
아니면 Team 객체의 members 가 수정되었을 때 update되어야 하는가?
JPA 에서는 이런 모호함을 없애기 위해
두 개의 참조 필드 중 하나가 외래키를 관리하는 룰을 만들었다.
그리고 외래키를 도맡아서 관리하는 하는 클래스의 필드를 우리는 연관관계의 주인이라고 한다.
위에서 본 Team 엔티티의 코드를 보면 @OneToMany
애노테이션의 인자값으로 mappedBy를 쓰는데, 이것은 자신이 연관관계의 주인이 아님을 의미한다.
그리고 mappedBy의 값은 실제 연관관계의 주인인 엔티티의 필드명(Member.team
)이다.
양방향 매핑에는 다음과 같은 규칙이 있으니 알아두자.
@ManyToOne
은 mappedBy 속성 자체가 없음 이건 답이 정해져 있다. 외래키가 있는 쪽이 주인이다.
이러면 헷갈리지도 않고, 성능 이슈도 없고 좋다.
이후 생길 수 있는 많은 고민거리가 해결된다.
그러니 "N:1의 관계에서는 N 쪽이 연관관계 주인"이라고 항상 생각하자.
연관관계 주인은 단순하게 DB 테이블에서 N 쪽이라고 생각하자.
순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하는 연관관계 편의 메소드를 작성하자.
연관관계 편의 메소드 작성시 알아둘 점
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {
@Id
@Column(name = "member_id")
private String id;
private String username;
// 연관관계 매핑
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
// 연관관계 편의 메소드
public void setTeam(Team team) {
if(this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
//... 생략 ...
}
@Entity
@Getter @Setter
@NoArgsConstructor
public class Member {
@Id
@Column(name = "member_id")
private String id;
private String username;
// 연관관계 매핑
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
public Member(String id, String username) {
this.id = id;
this.username = username;
}
// 연관관계 편의 메소드 - 사실은 setTeam 보다 더 비즈니스적인 이름을 줘야한다.
public void setTeam(Team team) {
if(this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
team.getMembers().add(this);
}
}
@Entity
@Getter @Setter
@NoArgsConstructor
public class Team {
@Id
@Column(name = "team_id")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public Team(String id, String name) {
this.id = id;
this.name = name;
}
}
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
// 팀 1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
em.persist(member1); //JPA에서 엔티티 저장시, 연관된 모든 엔티티는 "영속 상태"여야 함
// 회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1);
em.persist(member2);
em.flush();
em.clear();
// 조회 방법 2가지:
// 1.객체 그래프 탐색
/*
Member member1 = em.find(Member.class, "member1");
Team team = member1.getTeam();
System.out.println(team.getName());
*/
// 2. jpql
/*
String jpql = "select m from Member m join m.team t where " +
"t.name=:teamName";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setParameter("teamName", "팀1")
.getResultList();
for (Member member : resultList) {
System.out.println("member.getUsername() = " + member.getUsername());
}
*/
// 수정
/*
Team team2 = new Team("team2", "팀2");
em.persist(team2);
Member member1 = em.find(Member.class, "member1");
member1.setTeam(team2);
em.flush();
em.clear();
*/
// 연관관계 제거
/*
Member member1_ = em.find(Member.class, "member1");
member1_.setTeam(null);
em.flush();
em.clear();
*/
// 연관된 엔티티 삭제 - 연관관계 제거 후에 가능함! 외래키 제약 조건
/*
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null);
Member member2 = em.find(Member.class, "member2");
member2.setTeam(null);
Team team1 = em.find(Team.class, "team1");
em.remove(team1);
*/
tx.commit();
} catch (Exception e) {
e.printStackTrace();
tx.rollback();
} finally {
em.clear();
}
emf.close();
}
양방향 테스트는 작성하지 않았다.
양방향 테스트는 외래키를 직접 관리하는 필드와, 그렇지 않은 필드를 통해서
외래키 관리 여부를 보고, 연관관계 편의 메소드가 잘되는지 확인만 하면 된다.