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(500not 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(500not 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

반응형

'back > JPA' 카테고리의 다른 글

[JPA] equals , hashcode  (0) 2022.11.08
[JPA] JPQL, QueryDSL  (0) 2020.03.21
[JPA] JPA 영속성 컨텍스트  (0) 2020.03.08
[JPA] JPA 기초 설정 및 entity 필드 매핑  (0) 2020.02.27
[JPA] JPA란? : 등장배경, ORM, 하이버네이트  (0) 2020.02.23

+ Recent posts