공부/JPA
연관관계 매핑 기초
kd0547
2022. 11. 30. 21:28
목표
- 객체와 테이블 연관관계의 차이를 이해
- 객체의 참조와 테이블의 외래 키를 매핑
- 용어 이해
- 방향(Direction): 단방향, 양방향
- 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 대다(N:M) 이해
- 연관관계의 주인(Owner): 객체 양방향 연관관계는 관리 주인 이 필요
연관관계가 필요한 이유
객체를 테이블에 맞추어 모델링
(연관관계가 없는 객체)
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="member_id")
private Long id;
@Column(name="username")
private String username;
@Column(name="team_id")
private Long teamId;
}
@Entity
public class Team {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "team_id")
private Long id;
private String name;
}
- 테이블에 맞춰 외래키 값을 그대로 가지고 왔음
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeamId(team.getId());
em.persist(member);
Hibernate:
/* insert com.example.demo.domain.Team
*/ insert
into
Team
(name, team_id)
values
(?, ?)
Hibernate:
/* insert com.example.demo.domain.Member
*/ insert
into
Member
(team_id, username, member_id)
values
(?, ?, ?)
Q. 팀 ID를 이름을 조회할려면?
//memberId를 찾고
Member findMember= em.find(Member.class, member.getId());
// findMember에서 teamId를 찾아서
Long findTeamId = findMember.getTeamId();
// teamId로 요청을 보내야한다.
Team findTeam = em.find(Team.class, findTeamId);
문제점
💡 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
- 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
- 테이블과 객체 사이에는 이런 큰 간격이 있다.
단방향 연관관계
객체 지향 모델링
(객체 연관관계 사용)
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="member_id")
private Long id;
@Column(name="username")
private String username;
//@Column(name="team_id")
//private Long teamId;
@ManyToOne
@JoinColumn(name="team_id")
private Team teamId;
}
Member findMember= em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("findTeam.getName = "+ findTeam.getName());
//findTeam.getName = teamA
객체 지향 모델링
(연관관계 수정)
Member changeTeam = em.find(Member.class,1L);
// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
// 회원1에 새로운 팀B 설정
changeTeam .setTeam(teamB);
...
tx.commit();
- 회원에 팀이 변경된다.
양방향 연관관계와 연관관계의 주인 1 -기본
- Member에서 Team을 찾을 수 있음
- 그 반대는 불가능
양방향 매핑
- 양방향은 member → team , team → member로 조회할 수 있음
@Id @GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="member_id")
private Long id;
@Column(name="username")
private String username;
//@Column(name="team_id")
//private Long teamId;
@ManyToOne
@JoinColumn(name="team_id")
private Team team;
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
for(Member m : members) {
System.out.println("m = "+ m.getUsername());
}
연관관계의 주인과 mappedBy
- 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
객체와 테이블이 관계를 맺는 차이
- 객체 연관관계 = 2개
- 회원 -> 팀 연관관계 1개(단방향)
- 팀 -> 회원 연관관계 1개(단방향)
- 테이블 연관관계 = 1개
- 회원 <-> 팀의 연관관계 1개(양방향)
- 테이블은 FK키와 PK로 서로 조회 가능
객체의 양방향 관계
- 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단뱡향 관계 2개다.
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
테이블의 양방향 연관관계
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
- MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐 (양쪽으로 조인할 수 있다.)
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
Q. 새로운 팀에 들어가고 싶으면 Member 값을 변경해야할까? Team의 members를 변경해야할까
→ 둘 중 하나로 외래 키를 관리해야 한다.
연관관계의 주인(Owner)
양방향 매핑 규칙
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정
- 연관관계의 주인만이 외래 키를 관리(등록, 수정)
- 주인이 아닌쪽은 읽기만 가능
- 주인은 mappedBy 속성 사용X
- 주인이 아니면 mappedBy 속성으로 주인 지정
누구를 주인으로?
- 외래 키가 있는 있는 곳을 주인으로 정해라
- 여기서는 Member.team이 연관관계의 주인
@ManyToOne
@JoinColumn(name="team_id")
private Team team; //연관관계의 주인
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>(); //조회만 가능
양방향 연관관계와 연관관계의 주인 2 - 주의점, 정리
양방향 매핑시 가장 많이 하는 실수
(연관관계의 주인에 값을 입력하지 않음)
Member member = new Member();
member.setUsername("member1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member); //
em.persist(team);
Hibernate:
/* insert com.example.demo.domain.Member
*/ insert
into
Member
(team_id, username, member_id)
values
(?, ?, ?)
Hibernate:
/* insert com.example.demo.domain.Team
*/ insert
into
Team
(name, team_id)
values
(?, ?)
//결과
member_id username team_id
1 member1
- Team.members는 읽기 전용이라 결과가 반영되지 않는다.
양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
Team team = new Team();
team.setName("teamA");
Member member = new Member();
member.setUsername("member1");
//
member.setTeam(team);
em.persist(member);
//결과
member_id username team_id
1 member1 2
Q. 연관관계 주인만 값을 셋팅하면 될까?
- 순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야 한다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
//team.getMembers().add(member);
em.flush();
em.clear();
Team findTeam= em.find(Team.class, team.getId());
List<Member> members = findTeam.getMembers();
System.out.println("=========================");
for(Member m : members) {
System.out.println("m = "+m.getUsername());
}
System.out.println("=========================");
=========================
Hibernate:
select
members0_.team_id as team_id3_0_0_,
members0_.member_id as member_i1_0_0_,
members0_.member_id as member_i1_0_1_,
members0_.team_id as team_id3_0_1_,
members0_.username as username2_0_1_
from
Member members0_
where
members0_.team_id=?
m = member1
=========================
- 정상적으로 호출된다.
- for문 안에 m.getUsername()을 호출할 때 DB에서 값을 갖고오기 때문이다.
- team.getMembers().add(member);이 없어도 값이 출력된다.
- 여기서 2가지 문제점이 생긴다.
- em.flush(); em.clear(); 하면 문제는 없다.
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
//em.flush();
//em.clear();
Team fintTeam = em.find(Team.class, team.getId()); // 1차 캐시
List<Member> members = fintTeam.getMembers();
System.out.println("=========================");
for(Member m : members) {
System.out.println("m = "+m.getUsername());
}
System.out.println("=========================");
tx.commit();
//결과
=========================
=========================
- em.flush(); em.clear();을 주석처리한 경우
- team 객체는 1차 캐시에만 올라가게 된다.
- 이 경우 fintTeam.getMembers()을 호출해도 데이터가 없다.
- 영속성 컨텍스트에 초기 상태로 들어가 있기 때문이다.
Team team = new Team();
team.setName("teamA");
- team.getMembers().add(member);
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
team.getMembers().add(member);
//em.flush();
//em.clear();
Team findTeam = em.find(Team.class, team.getId()); // 1차 캐시
List<Member> members = findTeam.getMembers();
System.out.println("=========================");
for(Member m : members) {
System.out.println("m = "+m.getUsername());
}
System.out.println("=========================");
//결과
=========================
m = member1
=========================
양방향 연관관계 주의
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
- 연관관계 편의 메소드를 생성하자
- 양방향 매핑시에 무한 루프를 조심하자
- 예: toString(), lombok, JSON 생성 라이브러리
@Entity
public class Member {
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
- 양방향 관계 설정 시 team.getMembers().add(this); 설정하면
- team.getMembers().add(member);을 설정하지 않아도 된다.
//결과
=========================
m = member1
=========================
- 전 코드랑 차이가 없다.
양방향 매핑 정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
- JPQL에서 역방향으로 탐색할 일이 많음
- 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)