개요
- 방향(direction): 단방향, 양방향
- 다중성(multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
- 연관관계의 주인(owner): 객체 관계를 주도하는 주인을 지정
단방향 연관관계
객체 연관관계
- Member 객체는 Member.team 필드(멤버 변수)로 Team 객체와 연관관계를 맺는다.
- Member 객체와 Team 객체는 단방향 관계다.
- Member는 Member.team 필드를 통해서 Team을 알 수 있지만, 반대로 Team은 Member를 알 수 없다.
Member member = repository.findOne(id); Team team = member.getTeam();
테이블 연관관계
- 테이블은 외래 키(FK)로 연관관계를 맺음
- MEMBER 테이블은 TEAM_ID 외래 키로 TEAM 테이블과 연관관계를 맺는다. MEMBER 테이블과 TEAM테이블은 양방향 관계다.
- MEMBER 테이블의 TEAM_ID 외래 키를 통해서 MEMBER와 TEAM을 JOIN 할 수 있고, 반대로 TEAM과 MEMBER 도 JOIN 할 수 있다.
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.ID; SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID;
객체 연관관계와 테이블 연관관계
- 객체는 참조(주소)로 연관관계를 맺고, 테이블은 외래 키로 연관관계를 맺는다.
- 참조를 사용하는 객체의 연관관계는 단방향이고, 외래 키를 사용하는 테이블의 연관관계는 양방향이다.
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
객체 관계 매핑
- 객체 연관관계: Member 객체의 Member.team 필드 사용
- 테이블 연관관계: Member 테이블의 MEMBER.TEAM_ID 외래 키 칼럼을 사용
@Getter
@Setter
@Entity
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) {
this.team = team;
}
}
@Getter
@Setter
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
}
@JoinColumn
- 외래 키를 매핑할때 사용한다.
- 이 어노테이션은 생략할 수 있다.
- 이 어노테이션을 생략하면 외래키를 찾을 때 기본 전략을 사용한다.위 예제에서 생략 시 team_TEAM_ID 외래 키를 사용한다.
- 기본전략: 필드명 + _ + 참조하는 테이블의 칼럼명
@ManyToOne
- 다대일(N:1) 관계를 의미한다.
targetEntity 속성 예시
@OneToMany
private List<Member> members; // 제네릭으로 타입 정보를 알 수 있다.
@OneToMany(targetEntity=Member.class)
private List members; // 제네릭이 없으면 타입 정보를 알 수 없다.
- 연관관계 매핑 시 필수로 사용해야 한다.(다중성을 나타냄)
연관관계 사용
저장
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.
public void testSave() {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 회원1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
em.persist(member2);
}
INSERT INTO TEAM(TEAM_ID, NAME) VALUES ('team1', '팀1')
INSERT INTO MEMBER(MEMBER_ID, NAME, TEAM_ID) VALUES ('member1', '회원1', 'team1')
INSERT INTO MEMBER(MEMBER_ID, NAME, TEAM_ID) VALUES ('member2', '회원2', 'team1')
조회
- 객체 그래프 탐색
- 객체 연관관계를 사용한 조회, 객체를 통해 연관된 엔티티를 조회하는 것이다.
- 객체지향 쿼리(JPQL) 사용
'팀 1' FROM MEMBER MEMBER INNER JOIN TEAM TEAM1_ ON MEMBER.TEAM_ID = TEAM1_.ID WHERE TEAM1_.NAME = '팀1'
수정
private static void updateRelation(EntityManager em) {
// 새로운 팀2
Team team2 = new Team("team2", "팀1");
em.persist(team2);
// 회원1에 새로운 팀2 설정
Member member1 = em.find(Member.class, "member1");
member1.setTeam(team2);
}
UPDATE MEMBER
SET TEAM_ID = 'team2', ...
WHERE ID = 'member1'
- 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다.
- 연관관계를 수정할 때도 참조하는 대상만 변경하면 JPA가 자동으로 처리한다.
연관관계 제거
private static void deleteRelation(EntityManager em) {
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null); // 연관관계 제거
}
UPDATE MEMBER
SET TEAM_ID = null, ...
WHERE ID = 'member1'
연관된 엔티티 삭제
- 연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
member1.setTeam(null);
member2.setTeam(null);
em.remove(team1);
양방향 연관관계
@Getter
@Setter
@Entity
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) {
this.team = team;
}
}
@Getter
@Setter
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = Lists.newArrayList();
}
- mappedBy: 양방향 매핑일 때 사용, 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
public void biDirection() {
Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers(); // 팀 -> 회원, 객체 그래프 탐색
}
연관관계의 주인
- 객체 연관관계
- 회원 -> 팀 연관관계 1개(단방향)
- 팀 -> 회원 연관관계 1개(단방향)
- 테이블 연관관계
- 회원 <-> 팀 연관관계 1개(양방향)
- 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘의 차이가 발생한다.
- 연관관계의 주인 : 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리하는 것이다.
양방향 매핑의 규칙: 연관관계의 주인
- 두 객체 연관관계 중 연관관계의 주인을 정해서 테이블의 외래 키(FK)를 관리해야 한다.
- 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제) 할 수 있다.
- 주인이 아닌 쪽은 읽기만 할 수 있다.
- 연관관계 주인은 mappedBy 속성으로 지정
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
연관관계의 주인은 외래 키가 있는 곳
- DB 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.
양방향 연관관계의 주의점
- 양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
public void testSaveNonOwner() {
// 회원1 저장
Member member1 = new Member("member1", "회원1");
em.persist(member1);
// 회원2 저장
Member member2 = new Member("member2", "회원2");
em.persist(member2);
// 팀1 저장
Team team1 = new Team("team1", "팀1");
// 주인이 아닌 곳만 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(team1);
}
해당 코드를 수행하면 MEMBER 테이블의 TEAM_ID는 null로 저장되어 있다. 연관관계의 주인이 아니기 때문이다.
순수한 객체까지 고려한 양방향 연관관계
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
public void test순수한객체_양방향() {
// 팀1 저장
Team team1 = new Team("team1", "팀1");
Member member1 = new Member("member1", "회원1");
Member member2 = new Member("member2", "회원2");
member1.setTeam(team1); // 연관관계 설정 member1 -> team1
member2.setTeam(team1); // 연관관계 설정 member2 -> team1
log.info("member size: {}", team1.getMember().size()); // member size: 0
team1.getMembers().add(member1); // 연관관계 설정 team1 -> member1
team1.getMembers().add(member2); // 연관관계 설정 team1 -> member2
log.info("member size: {}", team1.getMember().size()); // member size: 2
}
Member와 Team에 양방향 관계를 설정했으면, 순수한 객체에서도 관계가 유지되어야 한다.
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
연관관계 편의 메서드
@Getter
@Setter
@Entity
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);
}
}
- 연관관계를 변경할 때는 기존 Team이 있으면 기존 Team과 Member의 연관관계를 삭제하는 코드를 추가해야 한다.
- this.team.getMembers(). remove(this); 코드가 없어도 외래 키를 변경하는 데는 문제가 없고, 새로운 영속성 콘텍스트에서도 변경된 팀이 조회된다.
- 문제는 관계를 변경하고 영속성 콘텍스트가 아직 살아있는 상태에서는 이전에 관계를 유지하고 있던 팀이 조회된다.
정리
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
- 양방향 매핑 시에는 무한 루프에 빠지지 않게 조심해야 한다.
반응형
'프로그래밍 > JPA' 카테고리의 다른 글
04장. Entity Mapping (6) | 2022.01.18 |
---|---|
03장. 영속성 관리 (2) | 2022.01.14 |
02. JPA 시작 (10) | 2022.01.11 |
01. JPA 소개 (4) | 2022.01.08 |
댓글