JPA에서의 연관관계 매핑
1. 단방향 매핑
Team 1 : User N 인 경우
User entity가 참조할 Team entity 매핑키(tid)를 넣어준다.
* 1:N 관계에서 N 테이블에 1테이블의 fk 를 가지고 있다.
관계의 주인은 N 테이블이 된다.
[Sample Code]
[Team entity]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
package com.jpp.webservice.web.domain.team;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import com.jpp.webservice.web.domain.user.User;
@Entity
public class Team {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long tid;
private String teamNm;
public Long getTid() {
return tid;
}
public void setTid(Long tid) {
this.tid = tid;
}
public String getTeamNm() {
return teamNm;
}
public void setTeamNm(String teamNm) {
this.teamNm = teamNm;
}
}
|
cs |
[User entity]
User 엔티티가 참조할 Team 의 tid 칼럼을 필드로 갖게한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package com.jpp.webservice.web.domain.user;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private String mobileNum;
private Long tid;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMobileNum() {
return mobileNum;
}
public void setMobileNum(String mobileNum) {
this.mobileNum = mobileNum;
}
public Long getTid() {
return tid;
}
public void setTid(Long tid) {
this.tid = tid;
}
}
|
cs |
[테스트]
User 를 저장할때 tid를 넣어주어 Team과 User를 매핑해준다.
실제로 참조관계를 설정해준건 아니기 때문에 연관관계가 있다고 하긴 모호하다.
일단 tid를 기준으로 User를 조회해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
package com.jpp.webservice.web.domain.team;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import com.jpp.webservice.web.domain.user.User;
import com.jpp.webservice.web.domain.user.UserRepository;
@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("test")
public class TeamRepositoryTest {
protected static final Logger LOGGER = LoggerFactory.getLogger(TeamRepositoryTest.class);
@Autowired
TeamRepository teamRepository;
@Autowired
UserRepository userRepository;
@Test
public void test() {
Team team = new Team();
team.setTeamNm("vip");
LOGGER.info("tid before save >>> " + team.getTid()); //save 를 하기 전엔 entity의 id(pk)가 생성되지 않는다
teamRepository.save(team);
Long tid = team.getTid();
LOGGER.info("tid after save >>> " + tid); //save 를 하고 난 후 entity의 id(pk)가 생성되어 있다
List<Team> teams = teamRepository.findAll();
for(Team t : teams) {
LOGGER.info("t nm:"+t.getTeamNm());
LOGGER.info("id:"+t.getTid());
}
User user = new User();
user.setMobileNum("01012345678");
user.setName("pyo");
user.setTid(tid); //team entity를 save 할 때 사용한 tid를 user entity 의 fk칼럼인 tid에 넣어준다
userRepository.save(user);
List<User> users = userRepository.findByName("pyo");
for(User u : users) {
LOGGER.info("u nm:"+u.getName());
LOGGER.info("u tid:"+u.getTid());
List<User> users2 = userRepository.findByTid(u.getTid());
for(User us : users2) {
LOGGER.info("us : " + us.getName());
}
}
}
}
|
cs |
[결과]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
Hibernate:
drop table posts if exists
Hibernate:
drop table team if exists
Hibernate:
drop table user if exists
Hibernate:
create table posts (
id bigint generated by default as identity,
author varchar(255),
content TEXT not null,
title varchar(500) not null,
primary key (id)
)
Hibernate:
create table team (
tid bigint generated by default as identity,
team_nm varchar(255),
primary key (tid)
)
Hibernate:
create table user (
id bigint generated by default as identity,
mobile_num varchar(255),
name varchar(255),
tid bigint,
primary key (id)
)
09:53:57.773 [main] INFO o.h.tool.hbm2ddl.SchemaExport - HHH000230: Schema export complete
09:54:00.266 [main] INFO c.j.w.w.d.team.TeamRepositoryTest - tid before save >>> null
Hibernate:
insert
into
team
(tid, team_nm)
values
(null, ?)
09:54:00.324 [main] INFO c.j.w.w.d.team.TeamRepositoryTest - tid after save >>> 1
09:54:00.344 [main] INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397: Using ASTQueryTranslatorFactory
Hibernate:
select
team0_.tid as tid1_1_,
team0_.team_nm as team_nm2_1_
from
team team0_
09:54:00.485 [main] INFO c.j.w.w.d.team.TeamRepositoryTest - t nm:vip
09:54:00.485 [main] INFO c.j.w.w.d.team.TeamRepositoryTest - id:1
Hibernate:
insert
into
user
(id, mobile_num, name, tid)
values
(null, ?, ?, ?)
Hibernate:
select
user0_.id as id1_2_,
user0_.mobile_num as mobile_n2_2_,
user0_.name as name3_2_,
user0_.tid as tid4_2_
from
user user0_
where
user0_.name=?
09:54:00.525 [main] INFO c.j.w.w.d.team.TeamRepositoryTest - u nm:pyo
09:54:00.525 [main] INFO c.j.w.w.d.team.TeamRepositoryTest - u tid:1
Hibernate:
select
user0_.id as id1_2_,
user0_.mobile_num as mobile_n2_2_,
user0_.name as name3_2_,
user0_.tid as tid4_2_
from
user user0_
where
user0_.tid=?
09:54:00.530 [main] INFO c.j.w.w.d.team.TeamRepositoryTest - us : pyo
|
cs |
위와같이 Team 에 넣은 tid 값을 기준으로 User를 조회할 수 있다.
중요한건 DDL 부분인데, pk만 지정되었을 뿐 참조관계, 즉 fk가 설정되지 않았다. (~28 Line 까지가 DDL 이며, 참조관계가 설정되는 구문이 보이지 않는다)
참조관계를 설정해줬다기 보단 User 테이블에 Team 테이블의 pk 칼럼을 포함시켰을 뿐이다.
(코드는 마치 fk 가 잡혀있듯이 개발하고, 실제 테이블엔 fk를 설정하지 않는 것 처럼)
그렇다면 진짜 참조관계(fk)를 설정해보자.
2. 양방향 매핑
1) 테이블에서의 양방향 매핑
- 외래 키 하나로 두 테이블 연관관계 관리
- MEMBER.TEAM_ID FK키 하나로 양방향 연관관계를 가지며 양쪽으로 조인이 가능하다
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
2) 객체에서의 양방향 매핑
- 객체의 양방향 관계는 Table의 양방향 관계와 달리 서로 다른 단방향 관계 2개(순환참조)로 만든다.
(위에서 살펴본 단방향 매핑 두개가 양쪽에 있는 개념)
Class A {
B b;
}
Class B {
A a;
}
[Sample Code]
[Team entity]
List<User> users 를 넣어주었다.
mappedBy는 말 그대로 매핑이 ?에 의해 이루어 진다는 의미로 참조를 당한다는 의미다.
@OneToMany는 1:N을 의미(앞쪽의 One 은 현재 엔티티 뒷쪽의 Many는 매핑된 다른 엔티티)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
package com.jpp.webservice.web.domain.team;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import com.jpp.webservice.web.domain.user.User;
@Entity
public class Team {
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private Long tid;
private String teamNm;
@OneToMany(mappedBy = "team")
private List<User> users = new ArrayList<User>();
public Long getTid() {
return tid;
}
public void setTid(Long tid) {
this.tid = tid;
}
public String getTeamNm() {
return teamNm;
}
public void setTeamNm(String teamNm) {
this.teamNm = teamNm;
}
public List<User> getUsers() {
return users;
}
public void setUsers(List<User> users) {
this.users = users;
}
}
|
cs |
[User entity]
Team team을 넣어주었다.
@JoinColumn 어노테이션을 달아 주었으며 이는 외부 엔티티의 필드를 참조함을 의미한다.
@ManyToOne은 N:1을 의미 (앞쪽의 Many가 현재 entity)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
|
package com.jpp.webservice.web.domain.user;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import com.jpp.webservice.web.domain.team.Team;
@Entity
public class User {
@Id
@GeneratedValue
private Long id;
private String name;
private String mobileNum;
@ManyToOne
@JoinColumn
private Team team;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMobileNum() {
return mobileNum;
}
public void setMobileNum(String mobileNum) {
this.mobileNum = mobileNum;
}
public Team getTeam() {
return team;
}
public void setTeam(Team team) {
this.team = team;
}
}
|
cs |
JPA Entity 매핑에선
1 Entity엔 @OneToMany(mappedBy= 1의 Table명)
N Entity엔 @ManyToOne @JoinColumn(id = 1의 Key)
ex) Member 1 : Order N 관계에서의 Entity 설정은 아래와 같다
@Entity
class Member {
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
@Entity
class Order {
@ManyToOne @JoinColumn(name = "member_id")
private Member member;
}
[테스트]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
package com.jpp.webservice.web.domain.team;
import java.util.List;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import com.jpp.webservice.web.domain.user.User;
import com.jpp.webservice.web.domain.user.UserRepository;
@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("test")
public class TeamRepositoryTest {
protected static final Logger LOGGER = LoggerFactory.getLogger(TeamRepositoryTest.class);
@Autowired
TeamRepository teamRepository;
@Autowired
UserRepository userRepository;
@Test
public void test1() {
Team team = new Team();
team.setTeamNm("vip");
teamRepository.save(team);
User user = new User();
user.setMobileNum("01012345678");
user.setName("pyo");
team.getUsers().add(user);
user.setTeam(team);
userRepository.save(user);
}
@Test
public void test2() {
List<Team> teamList = teamRepository.findAll();
for(Team t : teamList) {
for(User u : t.getUsers()) {
LOGGER.info("?!!");
LOGGER.info("user name : "+u.getName());
LOGGER.info("user team : "+u.getTeam());
}
}
}
}
|
cs |
[결과]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
16:15:34.405 [main] INFO o.h.tool.hbm2ddl.SchemaExport - HHH000227: Running hbm2ddl schema export
Hibernate:
drop table posts if exists
Hibernate:
drop table team if exists
Hibernate:
drop table user if exists
Hibernate:
create table posts (
id bigint generated by default as identity,
author varchar(255),
content TEXT not null,
title varchar(500) not null,
primary key (id)
)
Hibernate:
create table team (
team_id bigint generated by default as identity,
team_nm varchar(255),
primary key (team_id)
)
Hibernate:
create table user (
id bigint generated by default as identity,
mobile_num varchar(255),
name varchar(255),
team_id bigint,
primary key (id)
)
Hibernate:
alter table user
add constraint FKbmqm8c8m2aw1vgrij7h0od0ok
foreign key (team_id)
references team
16:15:34.431 [main] INFO o.h.tool.hbm2ddl.SchemaExport - HHH000230: Schema export complete
Hibernate:
insert
into
team
(team_id, team_nm)
values
(null, ?)
Hibernate:
insert
into
user
(id, mobile_num, name, team_id)
values
(null, ?, ?, ?)
16:15:37.128 [main] INFO o.h.h.i.QueryTranslatorFactoryInitiator - HHH000397: Using ASTQueryTranslatorFactory
Hibernate:
select
team0_.team_id as team_id1_1_,
team0_.team_nm as team_nm2_1_
from
team team0_
Hibernate:
select
users0_.team_id as team_id4_2_0_,
users0_.id as id1_2_0_,
users0_.id as id1_2_1_,
users0_.mobile_num as mobile_n2_2_1_,
users0_.name as name3_2_1_,
users0_.team_id as team_id4_2_1_
from
user users0_
where
users0_.team_id=?
16:15:37.264 [main] INFO c.j.w.w.d.team.TeamRepositoryTest - ?!!
16:15:37.264 [main] INFO c.j.w.w.d.team.TeamRepositoryTest - user name : pyo
|
cs |
참조 조건이 설정됨을 확인할 수 있다. (31~34 line)
Team 에서 User를 조회할 수 있다. (반대의 경우도 가능하다)
※ Owner : 연관관계의 주인
객체의 두 관계 중 하나를 연관관계의 주인으로 지정
연관관계의 주인만이 외래키를 관리(등록, 수정)
주인이 아닌 쪽은 읽기만 가능
주인은 mappeedBy 속성 사용하지 않음
주인이 아니면 mappedBy 속성으로 주인 지정
※ 일반적인 설계
위 예에서 User가 연관관계의 주인, 반대편인 Team은 가짜매핑(조회를 편하게 하기위한)
설계시 User의 Team을 주인으로 정한다. fk 키를 가지고 있는 칼럼을 주인으로 정해야 혼란을 방지할 수 있다.
※ 기타
양방향 매핑은 사실 필요가 없다.
조회를 편하게 하기 위함일 뿐, 일반적으론 단방향 매핑 관계만 적용하여 설계한다.
반대방향으로 조회가 필요한 경우, 그 때 양방향 관계(mappedBy)를 추가한다.
그리고 설계시 연관관계의 주인(JoinColumn)은 fk 키를 가지고 있는 entity의 필드를 주인으로 삼는다.
참고 :
Tacademy 김영한 개발자님의 강의
김영한 개발자님의 JPA 저서
https://www.baeldung.com/jpa-join-column