5장
연관관계 매핑 기초
연관관계 매핑을 이해하기 위한 핵심 키워드
관계를 가지고 있다는 의미는, 다른 엔티티를 자기 필드로 가지고 있다는 뜻이라고 생각해도 된다.
회원과 팀의 관계
회원과 팀이 있다고 가정.
한 회원은 한 팀만 가질 수 있고, 한 팀은 여러 회원을 가질 수 있다고 가정하면
회원 : 팀
N : 1 = 다대일 관계라고 표현
팀 : 회원
1 : N = 일대다 관계라고 표현
- 방향(direction)
- 단방향관계 : 회원이 팀을 필드로 가지고 있고, 팀은 회원을 필드로 가지고 있지 않는 경우가 있듯이 둘 중 한쪽만 참조하는 관계
- 보통 외래키가 있는 쪽은 무조건 관계를 가지고 있다.
- 양방향관계 : 회원이 팀을 필드로 가지고있고, 팀도 회원을 필드로 가지고 있는 경우. 둘 다 서로 참조하는 관계
방향은 객체지향 관계에서만 존재하고(애플리케이션 내)
테이블을 항상 양방향이다.
- 단방향관계 : 회원이 팀을 필드로 가지고 있고, 팀은 회원을 필드로 가지고 있지 않는 경우가 있듯이 둘 중 한쪽만 참조하는 관계
- 다중성(multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 다중성이 있다.
- 회원과 팀 관계 는 다대 일 관계
- 팀과 회원 관계는 일대 다 관계
- 연관관계의 주인(Owner) : 매우 중요. 객체를 양방향 연관관계를 만들면 연관관계의 주인을 정해야 한다.
- 외래 키를 가지고 있는 쪽이 연관관계의 주인! 명심!
5.1 단방향 연관관계
객체 연관관계
- 그림 상으로 회원 객체와 팀 객체를
단방향 관계
이다.- 회원 객체만 팀의 참조를 가지고 있다.
- 회원은 Member.team 필드를 이용해서 팀을 알수 있지만, 팀은 회원을 알 수 없다.(Member가 필드에 없기 떄문)
- Member.getTeam()으로 조회 가능, 그러나 Team.getMembers()는 조회 불가
테이블 연관관계
회원 테이블에서 TEAM_ID를 자신의 필드로 외래키를 가지고 있다. -> 팀 테이블과 연관관계를 맺음
테이블 상에서는 회원 테이블과 팀 테이블은
양방향 관계
TEAM_ID라는 키로 회원과 팀을 조인할 수 있고, 팀과 회원도 조인 가능.
select * from member m join team t m.team_id = t.team_id; // 멤버와 팀을 조인 select * from team t join member m where t.team_id = m.team_id; // 팀과 멤버를 조인 // 둘의 결과는 같다.
객체 연관관계와 테이블 연관관계의 가장 큰 차이는
참조를 통한 연관관계는 언제나 단 방향이다.
객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 서로 엔티티를 필드로 갖고있어야 참조가 가능하다.
하지만 정확히 이야기 하자면 양방향 관계까 아니라 서로 다른 단방향 관계 2개
이다
- 멤버 -> 팀 - 단방향 관계 1개
- 팀 -> 멤버 - 단방향 관계 1개
객체는 참조(엔티티 객체의 주솟값)으로 연관관계를 맺는다.
테이블은 외래키로 연관관계를 맺는다.
연관된 데이터를 조회할 때 객체는 참조(a.getB().getC())를 사용하지만 테이블은 조인을 사용한다
객체 그래프 탐색 : 연관관계에 있는 객체들은 필드로 보관하여 참조를 이용해 탐색하는것
테이블 조인 : 외래 키를 사용해서 연관관계를 탐색함
객체 - 관계 매핑
- 객체 연관관계 : 멤버 객체가 Team 객체를 가지고 있고, member.team 필드로 접근
- 테이블 연관관계 : 회원 테이블의 외래키인 TEAM_ID로 TEAM과 MEMBER를 조인해서 사용
여러 멤버는 한 팀에 속할 수 있다. - 다대일 (N:1) 관계
@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;
}
}
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
}
- 코드는 단 방향 관계 - 외래키가 존재하는 Member에만 Team의 참조를 보관중.
연관관계 매핑 어노테이션
- @ManyToOne : 다대일(N:1) 관계라는 매핑 정보
- @JoinColumn(name="TEAM_ID) : 조인 컬럼은 외래 키를 매핑할 때 사용. name 속성은 매핑할 외래키 이름 지정
- @JoinColumn 어노테이션으로 자기자신의 테이블에 매핑할 컬럼을 지정하는것이다.
- @JoinColumn을 갖고 있고, 외래키를 보유한 쪽이 연관관계의 주인
- @JoinColumn을
생략
하면 외래 키를 찾을 때 기본 전략을 사용한다- 기본 전략 : 필드명_참조하는 테이블의 컬럼명 -> team_TEAM_ID
속성 | 기능 | 기본값 |
---|---|---|
name | 매핑할 외래키 이름. 참조하는 엔티티의 기본키 필드명(참조하는 테이블의 컬럼명) |
참조하는 테이블의 기본 키 컬럼명 |
referencedColumnName | 외래키가 참조하는 대상 테이블의 컬럼명 | 참조하는 테이블의 기본키컬럼명 |
foreignKey | 외래키 제약 조건 직접 지정. 이 속성은 테이블 생성시에만 사용 |
|
unique, nullable, insertable, updatable, columnDefinition, table |
@Column의 속성과 같다. @Column 속성표 |
@ManyToOne의 속성
속성 | 기능 | 기본값 |
---|---|---|
optional | false로 설정하면 연관된 엔티티가 항상 있어야 한다. | true |
fetch | 글로벌 패치 전략 설정. 지연 로딩, 즉시 로딩 | @ManyToOne = FetchType.EAGER, @OneToMany = FetchType.LAZY |
cascade | 연속성 전이 기능. 연쇄 기능설정. | |
targetEntity | 연관된 엔티티의 타입 정보 설정. 이 전략은 거의 사용하지 않는다. (@ManyToOne(targetEntity = Member.class)) |
- @OneToOne (일대일 관계)도 존재
5.2 연관관계 사용
연관관계를 갖는 엔티티를 저장하는법
public void saveEntity() {
// 팀 1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 멤버1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정
em.persist(member1);
//멤버2저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계
em.persist(member2);
}
회원과 팀은 다대 일관계이다.
즉 여러 회원은 한 팀을 가지고, 한 팀은 여러 회원을 가질 수 있다
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속상태여야 한다. -> persist로 저장 해야하지 DB에 저장할 수 있다.
왜 팀을 먼저 저장하냐면, 팀의 기본키를 멤버에서 외래키로 사용하여 참조하기 때문이다.
팀 기본키가 먼저 생성되어야 멤버에서 팀을 참조할 수 있다.
SQL을 실행해보면, 팀이 먼저 생성 되고, 팀의 기본키값을 연관된 멤버1,2가 외래키로 참조하여 생성되는것을 볼 수 있다.
- persist나 save로 영속화 안하고 다른 엔티티와 관계를 맺으면 에러가 발생한다.
연관관계를 갖는 엔티티를 조회하는법
- 객체 그래프 탐색(필드 참조를 이용한 조회)
- 객체지향 쿼리 사용 (JPQL)
1. 객체 그래프 탐색
member는 team 객체를 필드로 보관하고 있다.
member.getTeam(); 으로 객체 그래프 탐색을 하여 team을 조회할 수 있다.
2. 객체지향 쿼리 사용 (JPQL)
String jpql = "select m from Member m join m.team t where t.name = : teamName";
List<Member> members = em.createQuery(jpql, Member.class)
.setParameter("teamName", "팀1")
.getResultList();
- sql 같은 jpql을 보면 문법이 약간 다르다.
- '*' 대신 m 이 들어간것을 보면 jpql은 DB를 향해 쿼리하는게 아닌 객체를 향해 쿼리해서 그런것.
- JPA가 플러시될 때 SQL로 바꿔준다.
- :teamName 은 teamName이란 파라미터를 넘겨주면 값을 세팅하는것
- 코드에서는 팀1을 세팅하므로 where t.name = '팀1'; 로 set이 된다.
연관관계를 갖는 엔티티를 수정하는법
private void updateRelation() {
Team team2 = new Team("team2", "팀2");
em.persist(team2);
//회원1에 새로운팀2 설정
Member member = em.find(Member.class, "member1");
member.setTeam(team2);
}
- 외래키 값을 team2의 기본키 값으로 UPDATE 하는 쿼리가 발생한다.
- 영속화된 엔티티의 값만 변경하면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동하여 수정된다.
연관관계를 제거하는법
private void deleteRelation() {
Member mebmer1 = em.find(Member.class, "member1");
member1.setTeam(null); // 연관관계 제거
}
null을 참조하게 하면, 연관관계가 있는 team의 외래키 값을 null로 수정하는 쿼리가 발생한다.
관계를 제거하는것이지, 삭제하는 것이 아니다.
연관된 엔티티 삭제
연관된 엔티티를 삭제하려면 기존에 있떤 관계를 먼저 제거하고 삭제해야 한다.
그렇지 않으면 외래 키 제약조건으로 인해 DB에서오류가 발생한다
확실하게 연관관계를 참조하는 필드를 null로 set하고 remove나 delete 메서드로 삭제해야 한다.
5.3 양방향 연관관계
단방향 연관관계는, 관계가 있는 두 엔티티가 한쪽에서만 다른 한쪽을 참조하지만,
양방향 연관관계는, 관계가 있는 두 엔티티가 필드로 서로를 참조하는것
일대 다 연관관계는 컬렉션
을 사용해야 한다.
- Collection, List, Set, Map 같은 다양한 컬렉션을 사용할 수 있다.
@Entity
public class Member {
@Id
@Column(name = "MEMBER_ID")
private String id;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
////////
@Entity
public class Team {
@Id
@Column(name = "TEAM_ID")
private String id;
@OneToMany(mappedBy = "team")
private List<Member> members;
}
- team.getMembers() 객체 그래프 탐색으로 팀에 속해있는 여러 회원들을 조회할 수 있다.
팀은 여러 멤버를 가질 수 있고, 멤버는 한 팀씩만 가질 수 있기 때문에,
멤버 입장에서는 여러 멤버(Many)가 한 팀을 가지는 (One), @ManyToOne
어노테이션을 사용하여 한 객체만 참조한다.
팀 입장에서는 한 팀(One)이 여러 멤버를 가지는(Many), @OneToMany
어노테이션을 사용하여 List로 참조한다.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나이기 때문에 차이가 발생한다.
관계가 두개인데 외래키는 1개이기 때문이다.
이 차이로 인해 JPA에서는 두 연관관계 중 하나를 정해서 테이블의 외래키를 관리하는데,
이것은 연관관계의 주인(Owner)
라고 한다.
- 명심하자. 연관관계의 주인은 외래키이다.
- 외래키를 자신이 가지고 있어야 등록 수정 삭제할 수 있다.
연관관계의 주인이란?
양방향 연관관계 매핑 시 지켜야 할 규칙이 있는데, 두개의 연관관계
중 하나
를 연관관계 주인
으로 정해야 한다.연관관계의 주인
만이 DB내의 연관관계와 매핑되고, 외래키를 관리(등록, 수정, 삭제)
할 수 있다.
반면에 주인이 아닌쪽은 읽기만
할 수 있다.
어떤 관계를 주인으로 정할지는 mappedBy
속성으로 정할 수 있다.
- 주인은 mappedBy 속성을 사용하지 않는다 (Member)
- 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 주인을 지정한다. ex) mappedBy=team
class Member {
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
}
class Team {
@OneToMany
private List<Member> members;
}
회원 엔티티에 있는 Member.team을 주인으로 선택하면 자기 테이블에 있는 외래키만 관리하면 되는데,
팀 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래키를 관리
해야 한다.
- 일대 다인데, 여러개의 일(회원)의 키를 다(팀)에서 관리할 수는 없다.
Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑 되어 있는데, 관리해야할 외래 키는 MEMBER 테이블에 있기 때문
연관 관계의 주인은(mapperdBy) 외래키가 있는곳으로 정해야 한다.
- 연관관계의 주인은 외래키 관리자 라는것.
TEAM 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다.
그러므로 주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정 해야 한다
class Team {
@OneToMany(mappedBy="team") // team은 Member 엔티티의 private Team team;
private List<Member> members;
}
정리하자면,
테이블은 외래 키 하나로 조인해서 두 테이블의 연관관계를 관리할수 있지만
객체에서 엔티티를 단방향 매핑하면 참조하는곳에서 외래키를 관리하면 된다.
그런데 양방향으로 매핑하면 두 곳에서 서로를 참조해서 연관관계의 관리포인트가 2곳으로 늘어나지만,
외래키는 단 1개 뿐이다. 그래서 테이블에서 외래키를 가지고 있고, 실제 테이블과 매핑되는 엔티티가 연관관계의 주인이 된다.
팀과 멤버가 있을 때 둘은 다대 일. 멤버는 외래키를 가지고 있다.
DB 테이블에서의 다대일, 일대다 관계에서는 항상 다 쪽이 외래키를 가진다.
외래키가 없는 쪽에는 @OneToOne, @OneToMany의 mapperedBy 속성을 사용.
- mapperedBy="team" 이란, 외래키가 있는 엔티티에서 자신을 참조하는 필드명을 지정하는것.
~ToOne 은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다.
~ToMany는 자기자신을 참조하니까 mappedBy를 설정할 수 있다.
연관관계의 주인은 외래 키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다
5.5 양방향 연관관계 저장
public void saveEntity() {
// 팀 1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
// 멤버1 저장
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정
em.persist(member1);
//멤버2저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계
em.persist(member2);
}
- 외래키가 Member 테이블에 있으므로 team의 키값인 외래키가 정상적으로 저장된다.
- 그래서 team1.getMembers().add(member1); 을 하지 않더라도, 외래키값은 정상적으로 저장된다.
- 주인이 아닌곳(외래키가 존재하지 않는곳)에 입력된 값은 외래키에 영향을 주지 않는다.
5.6 양방향 연관관계의 주의점
양방향 연관관계를 설정하고 가장 흔히 하는 실수는,
연관관계의 주인에는 값을 입력하지 않고 주인이 아닌 곳에만 값을 입력하는것.
즉 member.setTeam(team)은 하지 않고 team.getMembers().add(member);만 하는것
- 이러면 외래키 값이 정상적으로 저장되지 않는다.
그러면 양방향 연관관계에서 외래키가 존재하는 연관관계의 주인에만 값을 입력하고 주인이 아닌쪽에서는 값을 입력하지 않아도 되나?
- 아니다.
- 객체 관점에서는 양방향이기 떄문에 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전한다.
- 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 문제가 발생할 수 있다.
- member.getTeam()은 정상적으로 동작하지만, team.getMembers()는 원하는 멤버가 안나오거나 null이 나올수도 있다.
즉 양방향 연관관계
에서는 다음과 같이 관계를 맺는것이 안전하다.
public void saveEntity() {
// 팀 1 저장
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1); // 연관관계 설정 -> 외래키 설정
team1.getMembers().add(member1); // 연관관계 설정
em.persist(member1);
//멤버2저장
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1); // 연관관계 설정 - > 외래키 설정
team1.getMembers().add(member2); // 연관관계 설정
em.persist(member2);
}
- member1.setTeam(team1);
- 연관관계의 주인인 멤버가 팀과 관계를 맺음. 이때 외래키 저장
- team1.getMembers().add(member1);
- team은 연관관계의 주인이 아님. 저장시에 사용되지 않는다.
연관관계 편의 메서드
연관관계 편의 메서드는 양방향관계에서 관계를 맺을 때 조금 더 편리하고 안전하게 사용하는것이다.
public class Member {
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
- setTeam 메서드 하나로 양방향 관계를 모두 설정할 수 있다.
하지만
member1.setTeam(team1);
member1.setTeam(team2);
로 이와같이 연속적으로 setTeam을 호출한 후로 team1에서 멤버를 조회하면 member1이 여전히 조회되는 버그가 발생할 수 있다.
- team2로 변경할 때 team1에서는 member1와의 관계를 제거하지 않았기 때문
따라서 다음과 같이 팀을 정할 때, 기존 팀과의 관계를 제거하는 코드를 추가하는것이 좋다
public class Member {
@ManyToOne
private Team team;
public void setTeam(Team team) {
if (this.team != null) {
this.team.getMembers().remove(this); // 기존에 팀이 존재한다면 관계를 끊는다
}
this.team = team;
team.getMembers().add(this);
}
}
- 마찬가지로 팀에서도 멤버를 추가할 때 다음과 같이 검사하는것이 좋다.
public class Team {
@OneToMany(mapperedBy="team")
private List<Member> members;
public void addMember(Member member) {
if (this.members == null) {
this.members = new ArrayList();
}
if (!this.members.contain(member)) {
this.members.add(member);
member.setTeam(team);
}
}
}
연관관계 편의 메서드는 둘중 하나만 사용해도 된다.
5.7 정리
단방향 매핑보다 양방향 매핑이 매우 복잡하다.
연관관계의 주인도 정해야 하고, 연관관계 편의 메서드 등에서도 고려할 것이 많다.
양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된것 뿐이다.
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다. -> 외래키가 설정됨
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
- 양방향 매핑시에는 무한루프에 빠지지 않게 조심해야 한다.
- toString()사용시 서로를 계속 호출할 수 있으므로 주의할것.
- JSON으로 변환 할때도 조심해야 한다.
'스터디 > JPA 프로그래밍 스터디' 카테고리의 다른 글
JPA 프로그래밍 스터디 7장 정리 (1) | 2022.10.01 |
---|---|
JPA 프로그래밍 스터디 6장 정리 (0) | 2022.10.01 |
JPA 프로그래밍 스터디 4장 정리 (0) | 2022.09.13 |
JPA 프로그래밍 스터디 3장 정리 (0) | 2022.09.04 |
JPA 프로그래밍 스터디 1장,2장 (0) | 2022.08.31 |