참고
양방향 연관관계
NOTE
단방향 → 양방향이 된다는 것의 의미는 양쪽에서 서로를 참조할 수 있다는 것이다.
•
기존 단방향에서는 Member에서 getTeam()을 통해 Team을 조회할 수 있지만, Team에서는 그게 불가능했다.
•
양방향 연관관계는 양쪽에서 모두 조회가 가능해진다.
◦
Member Team (Member.team)
◦
Team Member(Team.members)
•
양방향을 만들어주기 위해 Team객체에 Member라는 List를 추가해서 넣어준다.
@Entity
public class Team{
...
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
...
}
Java
복사
양방향 연관관계의 주인
NOTE
엄밀히 말하면 객체에는 양방향 연관관계가 없다.
•
서로 다른 단방향 연관관계 2개를 어플리케이션 로직으로 잘 묶어서 양방향으로 보이게 하는것이다.
•
반면에 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 Join이 가능하다.
•
테이블은 외래키 하나만으로 양방향 연관관계를 맺는다!
객체와 테이블이 관계를 맺는 차이란?
NOTE
객체 연관관계 = 2개(단방향 2개 사용)
•
Member Team
•
Team Member
member.getTeam()
team.getMembers();
Java
복사
테이블 연관관계 = 1개
•
Team Member
•
데이터베이스 테이블은 외래키를 사용해 Join할 수 있기 때문
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
SQL
복사
양방향 매핑의 규칙
NOTE
엔티티를 양방향 연관관계로 설정하면 참조는 둘인데, 외래 키는 하나이다.
•
이럴 경우 어떤 값을 사용해서 외래키를 관리해야 하는가?
•
두 연관관계 중 하나를 연관관계의 주인으로 정한다!
◦
연관관계의 주인 : 데이터베이스와 매핑되고 외래 키를 관리(등록, 수정, 삭제) 가능해짐
◦
주인이 아님 :은 읽기만 가능하다.
•
연관관계의 주인은 mappedBy속성을 사용해서 정한다.
// Member
@ManyToOne // Meber 입장에서는 many, Team 입장에서는 one
@JoinColumn(name = "TEAM_ID")
private Team team;
Java
복사
// Team
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
Java
복사
주인이 아닌쪽에 mappedBy를 걸어준다. (값은 어떤 엔티티를 참조하는지를 의미)
•
양방향 연관관계 저장(Member가 주인인 경우)
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member.setTeam(team1); // 연관관계 설정 team1 -> team1
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
em.persist(member2);
Java
복사
정상적으로 처리됨
양방향 연관관계의 주인 = 외래 키가 있는 곳
NOTE
양방향 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다!
•
외래키가 있는 테이블을 주인으로 정하는 이유
◦
Member.team을 주인으로 선택하면, 자기 테이블의 외래 키를 관리하게 됨
◦
Team.members를 주인으로 선택하면, 다른 테이블의 외래 키를 관리하게 됨
•
여기서는 Member가 FK를 가지고 있으므로, Member가 주인이된다.
[ 참고]
•
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다!
◦
다 쪽인 @ManyToOne이 항상 주인이 되므로 mappedBy를 설정할 수 없다
◦
그래서 @ManyToOne은 mappedBy 속성이 없음
양방향 연관관계 주의점
NOTE
연관관계의 주인이 아닌 곳에서 값을 수정하면, 아무런 변경이 일어나지 않는다!
Member member1 = new Member("member1", "회원1");
em.persist(member1);
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team1 = new Team("team1", "팀1");
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(team1);
Java
복사
TEAM_ID에 값이 반영되지 않음!
•
연관관계의 주인인 Member.team은 아무 값도 입력 받지 않았으므로, TEAM_ID 외래 키의 값도 null이 들어간다.
순수한 객체까지 고려한 양방향 연관관계
NOTE
객체 관점에서 보면 양쪽 방향에 모두 값을 입력해주는 것이 안전하다.
•
JPA 관점에서는 주인쪽에서만 값을 저장하는 것은 맞지만, 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하는 순수 객체 상태에서 심각한 문제가 생길 수 있다.
문제 발생예시 (단방향 설정)
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1);
member2.setTeam(team2);
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
// 기대값 : member.size = 2
// 결과: member.size = 0
Java
복사
•
코드에서 Member.team에만 연관관계를 설정하고 반대방향은 설정하지 않았다.
◦
물론 commit할때 제대로 저장은 되지만, 순수 객체 상태일때가 문제다.
◦
1차캐시에는 아직 추가된 상태가 아니기 때문
문제 발생해결 (양방향 모두 설정)
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1);
team1.getMembers().add(member1);
member2.setTeam(team2);
team1.getMembers().add(member2);
List<Member> members = team1.getMembers();
System.out.println("members.size = " + members.size());
// 결과: member.size = 2
Java
복사
연관관계 편의 메서드
NOTE
•
양방향 연관관계는 결국 양쪽 모두 신경 써야하고 만약 실수한다면 양방향이 깨질 수 있다.
•
그래서 이를 해결하는 메소드(연관관계 편의 메서드)를 생성한다.
public class Member{
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
// ...
}
Java
복사
◦
setTeam()함수를 정의해서, 한 쪽을 수정하면 반대쪽도 한번에 수정되도록 한다.
[ 주의사항]
•
양방향 매핑시 무한 루프를 조심( Ex: toString(), lombk , JSON 생성 라이브러리 )
toString 무한 루프코드