[자바 ORM 표준 JPA 프로그래밍] 5장. 연관관계 매핑 기초
by Jo
5장. 연관관계 매핑 기초
- object는 reference로 연관관계
- table은 foreign key로 연관관계
- 서로 다른 둘을 mapping 하는데에는 다음 세 가지가 핵심 키워드
- Direction: 참조하는 방향. 한 쪽만 참조하면 단방향, 서로 참조시 양방향(reference를 통한 연관관계은 항상 단방향임. 서로 참조시에도 unidrectional 연관관계가 두개인 것임). table은 항상 양방향
- Multiplicity: 1:1, 1:N, N:1, N:M
- Owner: 양방향 연관관계al 만들 경우 owner 설정해야 함
5.1 단방향
- member & team
- member는 하나의 team 에만 소속 가능
- ∴ member와 team 은 N:1 관계
- object 연관관계
- member object는 Member.team field로 team object와 연관관계
- member와 team은 단방향
- object는 reference를 통해 연관관계 탐색 -> 객체 그래프 탐색
- member 는 member.team 으로 team 을 조회할 수 있지만 team 에선 불가능함
- reference를 통한 연관관계은 항상 단방향
- table 연관관계
- member table은 team_id foreign key로 team table 과 연관관계
- member table과 team table 은 양방향
- team_id 키로 join해서 member 와 team, team 과 member 조회 모두 가능
5.1.1 object 연관관계 mapping
코드
// code
@Entity
public class Membnber {
@Id
private String id;
private String username;
// 연관관계 mapping
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
// 연관관계 설정
public void setTeam(Team team) {
this.team = team;
}
...
}
...
@Entity
public class Team {
@Id
private Stirng id;
private String name;
...
}
- object 연관관계: member object의 Member.team field 사용
- table 연관관계: member table 의 member.team_id fk 사용
- Member.team과 member.team_id 를 mapping 하는 것이 연관관계 mapping
@JoinColumn
속성 | 기능 | 기본값 |
---|---|---|
name | mapping 할 fk 이름 | {field 명}_{참조하는 table의 pk column 명} |
referencedColumnName | fk가 참조하는 대상 table의 column 명 | 참조하는 table의 pk column 명 |
foreignKey(DDL) | fk constraints 직접 지정 가능. table 생성시에만 사용 | |
unique | @Column 속성과 동일 | |
nullable | @Column 속성과 동일 | |
insertable | @Column 속성과 동일 | |
updatable | @Column 속성과 동일 | |
columnDefinition | @Column 속성과 동일 | |
table | @Column 속성과 동일 |
@JoinColumn 생략
생략시 default strategy 사용하여 fk 찾음
{field 명}_{참조하는 table의 pk column 명}=fk 이름
따라서 위 예제 같은 경우 fk는 알아서 team_id가 됨
@ManyToOne
- N:1 연관관계에서 사용
속성 | 기능 | 기본값 |
---|---|---|
optional | false 설정시 연관된 entity가 항상 있어야 함(non nullable) | true |
fetch | global fetch strategy 설정 | @ManyToOne: FetchType.EAGER @OneToMany: FetchType.LAZY |
cascade | 영속성 전이 기능을 사용 | |
targetEntity | 연관된 entity의 타입 정보를 설정. 거의 안씀. collection 사용해도 generic으로 타입 정보 알 수 있음 |
- targetEntity 속성 사용 예시
코드
@OneToMany
private List<Member> members; // generic으로 타입 정보 알 수 있음
@OneToMany(targetEntity=Member.class)
private List members // genreic 없으면 타입 정보 알 수 없음. 그래서 targetEntity로 타입 정보 설정
5.2 연관관계 사용
저장
코드
// code
Team team1 = new Team("team1", "Alpha");
em.persist(team1); // !! jpa 에서 entity 저장 시 연관된 모든 entity 는 영속 상태여야 함 !!
Member member1 = new Member("member1", "Alice");
member1.setTeam(team1);
em.persist(member1);
Member member2 = new Member("member2", "Bob");
member2.setTeam(team1);
em.persist(member2);
sql
# sql
INSERT INTO TEAM (id, name) VALUES ('team1', 'Alpha');
INSERT INTO MEMBER(id, username, team_id) VALUES ('member1', 'Alice');
INSERT INTO MEMBER(id, username, team_id) VALUES ('member2', 'Bob');
5.2.2 조회
- 연관관계sihp 있는 entity 조회 방법은 크게 다음 2가지임
- 객체 그래프 탐색
- JPQL(객체지향 쿼리) 사용
- 객체 그래프 탐색
- 객체를 통해 연관된 entity를 조회하는 방법
-
위 예제와 같은 경우 다음과 같이 member와 연관된 team 조회
Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
</details>
- JPQL
- 특정 team 에 소속된 member 만 조회하려면 member와 연관된 team entity 를 검색 조건으로 사용
- SQL은 연관 table 을 join 해서 검색조건 사용하면 된다
- JPQL도 join 을 지원
- 특정 team 에 소속된 member 만 조회하려면 member와 연관된 team entity 를 검색 조건으로 사용
// code
private static void queryLogicJoin(EntityManager em) {
// member가 team 과 연관관계 가지고 있는 field인 m.team 을 통해서 member 와 team join
// :teamName 처럼 :로 시작하는 것은 parameter binding 문법
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", "Alpha")
.getResultList();
}
</details>
<details>
# sql
SELECT m.*
FROM member m
INNER JOIN team t
ON m.team_id = t.id
WHERE t.name = 'Alpha'
</details>
5.2.3 수정
-
일반적인 entity 수정과 마찬가지로 entity 가 참조하는 대상만 변경해 두면 이후 tarnsaction commit 시 flush 일어나면서 변경 감지를 통해 알아서 처리됨
// code
Team team2 = new Team("team2", "Beta");
em.persist(team2);
Member member = em.find(Member.class, "member1");
member.setTeam(team2);
</details>
<details>
# sql
UPDATE member
SET team_id = 'team2', ...
WHERE id = 'member1'
</details>
5.2.4 연관관계 제거
코드
// code
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null); // 연관관계 제거
sql
# sql
UPDATE member
SET team_id = null
WHERE id = 'member1'
5.2.5 연관된 entity 삭제
- 연관된 entity 삭제하려면 먼저 기존에 있던 연관관계를 제거해야 함
-
서순 틀리면 fk constraint 로 인해 DB error 발생
-
// 위 예제에서의 team Alpha 삭제하려면 먼저 소속 member에서의 reference를 모두 지워서 연관관계 끊어야 함
member1.setTeam(null);
member2.setTeam(null);
em.remove(team);
</details>
5.3 양방향 연관관계
- member -> team (Member.team) 은 N:1
- team -> member (Team.members) 는 1:N
- 1:N 관계는 여러 건과 연관관계 맺을 수 있으므로 collection 사용해야 함
- ∴
Team.members
를 List collection 으로 추가(List 포함 Collection, Set, Map 등 다양하게 지원)
5.3.1 양방향 연관관계 mapping
코드
@Entity
public class Member {
@Id
private Stirng 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
private String id;
private String name;
// 1:N 연관관계 설정
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<Member>();
...
}
@OneToMany
의 mappedBy 속성은 양방향 mapping 일 때 사용- 반대쪽 mapping 의 field 이름을 값으로 하면 됨
5.4 연관관계의 owner
- entity의 연관관계를 양방향으로 설정 시 객체 참조는 둘, foregin key는 하나
- 차이가 발생
- ∴ owner를 정해야 함
5.4.1 양방향 mapping의 규칙: 연관관계의 owner
- bidriection 연관관계 mapping 시 두 연관관계 중 하나는 owner로 정해야 함
- owner 만이 DB 연관관계과 mapping 되고 foriegn key를 관리(등록, 수정, 삭제) 가능
- owner 아닌 쪽은 read only
- 이 때 연관관계 owner 정해주는게 mappedBy 속성
- owner 아닌 쪽에서 owner 를 mappedBy 속성 값으로 지정해주면 됨
- members -> team
코드
@Entity
class Member {
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
...
}
- team -> members
코드
@Entity
class Team{
@OneToMany
private List<Member> members = new ArrayList<Member>();
...
}
- team entitiy의 Team.members 를 owner로 설정하는 경우
- 다른 테이블(member)에 있는 foreign key를 관리해야 함
- member entity의 Member.team 을 owner로 설정하는 경우
- 자기 테이블에 있는 foreign key 관리하면 됨
- 따라서 ownere는 foreign key가 있는 곳으로 설정해야 함
- 위 예시 같은 경우에는 Member.team 을 owner로 설정
DB table의 N:1, 1:N 관계에서는 항상 N 쪽이 foreign key 가짐 >br/> 따라서 @ManyToOne 은 항상 owner side 이기 때문에 mappedBy 속성이 없음
5.5 양방향 연관관계 저장
코드
// team1 저장
Team team1 = new Team("team1", "Alpha");
em.persist(team1);
// member1 저장
Member member1 = new Member("member1", "Alice");
// 연관관계 설정: Member(Alice) -> Team(Alpha)
member1.setTeam(team1);
em.persist(member1);
// member2 저장
Member member2 = new Member("member2", "Bob");
// 연관관계 설정: Member(Bob) -> Team(Alpha)
member2.setTeam(team1);
em.persist(member2);
id | username | team_id |
---|---|---|
member1 | Alice | team1 |
member2 | Bob | team1 |
- 위와 같이 owner side인 Member.team에 연관관계인 Team 설정해주면 member table의 foreign key가 알아서 설정됨
5.6 양방향 연관관계 주의점
- 양방향 연관관계 설정시 non-owning side 에만 값 넣으면 제대로 반영이 안됨
- owner side 에서 mapping된 foreign key 관리하기 때문에 owner side에서 값 설정 안해주면 DB에 안들어감
5.6.1 순수한 객체까지 고려한 양방향 연관관계
- 다만 객체 관점에서 보았을 때 양쪽 방향 모두 값 입력해 주는 것이 안전
- DB에는 제대로 저장되기 때문에 해당 persistence context 종료 후 새로 만들어 조회하면 정상적으로 사용
- 하지만 그 전까지 순수한 객체 상태일 때는 값 넣어주지 않은 쪽에서는 연관 객체 접근이 불가능
- 의도한 대로 동작 X
- ∴ 아래와 같이 양 쪽에 모두 값 넣어주는 것이 좋음
코드
// team1 저장
Team team1 = new Team("team1", "Alpha");
em.persist(team1);
// member1 저장
Member member1 = new Member("member1", "Alice");
// 연관관계 설정: Member(Alice) -> Team(Alpha)
member1.setTeam(team1);
// 연관관계 설정: Team(Alpha) -> Member(Alice)
team1.getMembers().add(member1);
em.persist(member1);
// member2 저장
Member member2 = new Member("member2", "Bob");
// 연관관계 설정: Member(Bob) -> Team(Alpha)
member2.setTeam(team1);
// 연관관계 설정: Team(Alpha) -> Member(Bob)
team1.getMembers().add(member2);
em.persist(member2);
5.6.2 연관관계 편의 method
- 위 예시처럼 양방향 연관관계 에서는 양쪽 모두 값 넣어줘야 함
- 직접 넣는 방식은 실수할 확률이 높음
- ∴ 한번에 양 쪽 다 넣어주는 method 작성해서 쓰는게 좋음
- 이를 연관관계 편의 method라 부름
코드
@Entity
public class Member {
...
private Team team;
...
public void setTeam(Team team) {
// 이미 다른 team과 연관관계 있는 member일 수 있으므로 꼭 확인하고 있으면 관계 제거해야 함
if (this.team != null) {
this.team.remove(this);
}
this.team = team;
team.getMembers().add(this);
}
...
}
- 연관관계 편의 method 작성시 기존 연관관계를 확인하고 제거해야 함
- owner side에만 값 넣어주는 경우와 마찬가지로 이것도 persistence context 종료 후 새 context 에서 조회 시엔 정상적으로 작동하지만 그 전까지는 연관관계 해제된 entity가 조회되기때문에 문제
Subscribe via RSS