개요
엔티티는 대부분 다른 엔티티와 연관관계가 있다. 그런데 객채는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다. 이 둘은 완전히 다른 특징을 가짐.
객체 관계매핑(ORM)에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일이다.
객체와 참조와 테이블의 외래 키를 매핑하는 것이 이 장의 목표이다.
- 방향(direction) : 단방향,양방향이 있다. 방향은 객체 관계에만 존재. 테이블 관계는 항상 양방향
- 다중성(Multiplicity): N:1, 1:N, 1:1 , N:M
- 연관관계의 주인(owner): 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.
객체 연관관계 vs 테이블 연관관계
객체는 참조(주소)로 연관관계
테이블은 외래키로 연관관계.
참조를 통한 연관관계는 언제나 단방향이다.
1
2
3
4
5
6
7
8
9
|
class A{
B b;
}
class B{
A a;
}
-A -> B(a,b)
-B -> A(b,a)
|
cs |
양방향 관계 X. 서로다른 단방향 관계 2개
외래 키를 사용하는 테이블의 연관관계
-A JOIN B가 가능하면 반대로 B JOIN A도 가능
객체 관계 매핑
//매핑
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
매핑한 팀 엔티티
@Entity
public class Team{
@Id
@Column(name= "TEAM_ID")
private String id;
}
객체 연관관계:회원 객체의 Member.team 필드 사용
테이블 연관관계: 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용
Member.team과 MEMBER.TEAM_ID를 매핑하는것이 연관관계 매핑이다.
@JoinColumn
name: 매핑할 외래 키 이름
referencedColumnName : 외래 키가 참조하는 대상 테이블의 컬럼명
foreignKey(DDL) : 외래 키 제약조건 직접 지정. (테이블 생성시만 사용)
+@Column의 속성.
@JoinColumn
optional: false 시 연관된 엔티티 항상 있어야함.기본값 true
fetch: 8장
cascade: 8장
MEMBER_ID | NAME | TEAM_ID | TEAM_NAME |
member1 | 회원1 | team1 | 팀1 |
member2 | 회원2 | team2 | 팀1 |
단방향
조회
-객체 그래프 탐색
Member member=em.find(Member.class,"member1");
Team team = member.getTeam();
-객체지향 쿼리 사용(JPQL)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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){
}
//출력시
member.username=회원1
member.username-회원2
|
cs |
:teamName과 같이 :로 시작하는 것은 파라미터를 바인딩받는 문법이다.
실행되는 SQL은 다음과 같다.
SELECT M. * FROM MEMBER MEMBER
INNER JOIN
TEAM TEAM ON MEMBER.TEAM_ID=TEAM1_.ID
WHERE
TEAM1_.NAME='팀1'
수정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
//새로운 팀
Team team2= new Team("team2","팀2");
em.persist(team2);
//회원1에 새로운 팀2 설정
Member member = em.find(Member.class,"member1");
member.setTeam(team2);
실행되는 수정 SQL
UPDATE MEMBER
SET
TEAM_ID='team2',...
WHERE
ID='member1'
|
cs |
수정은 엔티티 값 변경시 트랜잭션 커밋할 때 플러쉬 일어나면서 변경 감지 기능이 작동하므로 update같은 메소드 필요 없음.
연관관계 제거
Member member1=em.find(Member.class,"member1");
member1.setTeam(null); 연관관계 제거
SQL
UPDATE MEMBER
SET
TEAM_ID=null,
WHERE
ID='member1'
연관된 엔티티 삭제
연관된 엔티티 삭제하려면 기존에 있던 연관관계 먼저 제거하고 삭제해야 한다.
그렇지 않으면 외래 키 제약조건으로 인해 db오류 발생.
팀1에는 회원1과 회원2가 소속되어 있다. 이때 팀1을 삭제하려면 연관관계를 먼저 끊어야 함.
member1.setTeam(null); //회원1 연관관계 제거
member2.setTeam(null); //회원2 연관관계 제거
em.remove(team); //팀 삭제
양방향 연관관계
일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.
객체 연관관계에서 Team.members를 List 컬렉션으로 추가
-회원=>팀 (Member.team)
-팀=>회원(Team.members)
테이블 연관관계는 외래키 하나로 양방향 조회가능.
양방향 연관관계 매핑
//==추가==//
@OneToMany(mappedBy="team")
private List<Member> members = new ArrayList<Member>();
일대다 관계 매핑위해 @OneToMany 매핑 정보 사용.
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
반대쪽 매핑이 Member.team이므로 team을 값으로 줌.
일대다 컬랙션 조회
Team team= em.find(Team.class , "team1");
List<Member> members= team.getMembers();
연관관계의 주인
mappedBy는 왜 필요할까?
엄밀히 이야기하면 객체는 양방향 연관관계 없음.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다.
따라서 둘 사이에 차이가 발생한다.
-> 이런 차이로인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을
연관관계의 주인(Owner)라고 한다.
연관관계의 주인만이 db 연관관계와 매핑되고 외래 키를 관리 (등록,수정,삭제)할 수 있다.
반면 주인이 아닌 쪽은 읽기만 할 수 있다.
- 주인은 mappedby 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
그렇다면 Member.team, Team.members 둘 중 어떤 것을 연관관계의 주인으로 정해야 할까?
다음 두 코드를 보자.
-회원 -> 팀(Member.team) 방향
1
2
3
4
5
6
7
|
class Member{
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
}
|
cs |
-팀 -> 회원(Team.members) 방향
1
2
3
4
5
6
|
class Team{
@OneToMany
private List<Member> members= new ArrayList<Member>();
}
|
cs |
연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것.
여기서는 회원 테이블에 있는 TEAM_ID 외래 키를 관리자를 선택해야한다.
-회원 엔티티에 있는 Member.team을 주인으로 선택시:
자기테이블에 있는 외래 키를 관리하면 된다.
-팀엔티티에 있는 Team.members를 주인으로 선택시:
물리적으로 전혀 다른 테이블의 외래 키를 관리해야 한다. 왜냐 하면 이 경우 Team.members가 있는 Team 엔티티는
TEAM테이블에 매핑되어 있는데 관리해야할 외래 키는 MEMBER테이블에 있기 때문.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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);
}
|
cs |
회원1,회원 2에 연관관계의 주인인 Member.team 필드를 통해 연관관계 설정하고 저장함.
이 코드는 단방향 연관관계에서 살펴본 예제 코드와 같다.
양방향 연관관계의 주의점
team1.getMembers().add(member1);
team1.getMembers().add(member2);
SELECT * FROM MEMBER;
회원을 조회한 결과 TEAM_ID가 null값으로 저장됨.
연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문.
연관관계의 주인만이 외래 키의 값을 변경할 수 있다.
예제코드는 Member.team에 아무 값도 입력하지 않았다. -> 따라서 TEAM_ID외래 키의 값도 null이 저장된다.
but
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
why?
양방향 비 설정시 team1.getMembers().size()가 0이 나올수 있음.
member1.setTeam(team1);
team1.getMembers().add(member1);
em.persit(member1);
양방향 연관관계는 결국 양쪽 다 신경 써야 한다.
각각 호출하면 실수로 양방향 깨질 수 있으므로
Tip:두코드는 하나인 것처럼 사용하는 것이 안전하다.
연관관계 편의 메소드 작성 시 주의사항
member1.setTeam(team1);
member1.setTeam(team2);
Member findMember=teamA.getMember(); //member1이 여전히 조회됨.
1
2
3
4
5
6
7
8
9
|
public void setTeam(Team team){
//기존 팀과 관계를 제거
if(this.team != null){
this.team.getMembers().remove(this);
}
this.team= team;
team.getMembers().add(this);
}
|
cs |
이 코드는 객체에서 서로 다른 단방향 연관관계 2개를 양방향인 것처럼 보이게 하려고 얼마나 많은 고민과
수고가 필요한지 보여준다. 반면에 관계형 db는 외래 키 하나로 문제를 단순하게 해결한다.
결론: 객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 한다.
정리
양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.
member.getTeam();
team.getMembers();
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
'자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
7장 (0) | 2021.05.04 |
---|---|
6장 (0) | 2021.05.02 |
4장 (0) | 2021.05.01 |
1장 JPA란 무엇인가? (0) | 2021.04.30 |
1장 JPA를 사용하는 이유(비교) (0) | 2021.04.30 |