객체
와테이블
연관관계의 차이를 이해객체의 참조
와테이블의 외래 키
를 매핑- 용어 이해
방향(Direction)
: 단방향, 양방향다중성(Multiplicity)
: 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해연관관계의 주인(Owner)
: 객체 양방향 연관관계는 관리 주인이 필요 (제일 어렵)
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다
📌객체를 테이블에 맞추어 모델링
객체를 테이블에 맞추어 모델링하면 연관관계가 없는 객체가 된다.
- Member 테이블의 외래키
TEAM_ID
를 그대로 모델링
TEAM객체를 참조하는게 아니라 그냥 외래키 값
TEAM_ID
을 그대로 사용한다.
member.setTeamId(team.getId());
: 왜래키 식별자를 직접 다룬다.
연관관계가 없기 때문에 식별자로 다시 조회해야한다. 👉 객체지향적인 방법이 아니다
📌객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다
외래 키로 조인
을 사용해서 연관된 테이블을 찾는다.참조
를 사용해서 연관된 객체를 찾는다.
TEAM_ID
가 아닌TEAM
이라는 참조값을 사용한다. (객체 연관관계를 사용)
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne // member : team = n : 1 (다대일 관계)
@JoinColumn(name = "TEAM_ID") // 객체 연관관계 team 참조와 테이블 연관관계 TEAM_ID 외래키와 매핑
private Team team;
…
}
@ManyToOne
: member : team = n : 1 (다대일 관계)@JoinColumn(name = "TEAM_ID")
: 객체 연관관계 team 참조와 테이블 연관관계 TEAM_ID 외래키와 매핑
👉 ORM 매핑이 완성됨
JPA가 team에서 pk값을 추출하여 member의 fk로 설정한다.
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team);
em.persist(member);
// findMember를 영속성 컨택스트의 값이 아닌 DB의 값으로 채우기 위해서
// flush로 DB에 쿼리를 날리고
// clear로 영속성 컨택스트를 깨끗이 초기화한다
System.out.println("em.flush()");
em.flush();
em.clear();
//조회
System.out.println("참조를 사용해서 연관관계 조회");
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
System.out.println("findTeam = " + findTeam.getName());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
member.setTeam(team);
member
에team 참조
를 저장하면 JPA는 데이터베이스의 MEMBER 테이블의 fk값으로 TEAM 테이블의 pk을 설정한다.
em.persist(member);
: member를 영속성 컨택스트의 1차 캐시에 저장em.find(Member.class, member.getId());
: 영속성 컨택스트의 1차 캐시에서 member 객체를 가져온다
- 1차 캐시가 아닌 데이터베이스에서 가져오고 싶다면
em.flush(); em.clear();
를 해준다.- 로그를 보면
member
를 조회할 때member
,team
을 한번에 조인해서 가져온다는 것을 알 수 있다.findMember.getTeam();
: 참조로 연관관계를 조회할 수 있다 👉 객체 그래프 탐색을 활용할 수 있다.
연관관계를 수정할 수 있다.
- 테이블 연관관계
단방향 테이블 연관관계
과양방향 테이블 연관관계
의 차이가 없다.- 그냥 두 테이블에서 pk, fk를 활용하여 조인하면 된다.
👉 테이블은 외래키 하나만으로 양방향 연관관계가 이뤄진다.- 객체 연관관계
단방향 객체 연관관계
- Member에서는
Team team
으로 team 참조가 가능하다.- Team에서 Member로 참조할 수 있는 길이 없다.
양방향 객체 연관관계
- Team의
List members
를 통해 Member로 참조한다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne // member : team = n : 1 (다대일 관계)
@JoinColumn(name = "TEAM_ID") // 객체 연관관계 team 참조와 테이블 연관관계 TEAM_ID 외래키와 매핑
private Team team;
…
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
// member : team = n : 1 이므로 OneToMany
// (mappedBy = "team") : Member 클래스의 team이라는 변수와 매핑
private List<Member> members = new ArrayList<>();
}
Team은
List<Member>
컬랙션을 추가해주어 양방향 매핑을 한다.
👉 이제 Team에서 Member로 참조할 수 있다.
@OneToMany(mappedBy = "team")
- member : team = n : 1 이므로
@OneToMany
- Member 클래스의 team이라는 변수와 매핑하기 위해
mappedBy = "team"
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
//역방향 조회
for (Member m : members) {
System.out.println("m.getUserName = " + m.getUserName());
}
Team findTeam = em.find(Team.class, team.getId());
//역방향 조회
int memberSize = findTeam.getMembers().size();
System.out.println("memberSize = " + memberSize);
객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
- 객체 연관관계 = 2개
회원 -> 팀
연관관계 1개(단방향Team team
)팀 -> 회원
연관관계 1개(단방향List<Member> members
)
👉 단방향 연관관계가 사실상 2개 있다 (억지로 양방향이라고 일컬을 뿐)- 테이블 연관관계 = 1개
회원 <-> 팀
의 연관관계 1개(양방향TEAM_ID (pk)
,TEAM_ID (fk)
)
- 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단뱡향 관계 2개다.
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
MEMBER.TEAM_ID
외래 키 하나로 양방향 연관관계 가진다. (양쪽으로 조인할 수 있다.)
멤버를 바꾸거나 새로운 팀에 들어가려 할 때, MEMBER 테이블의
TEAM_ID fk
는 언제 업데이트 해야할까❓
- Member 객체의
Team team
을 변경했을 때?- Team 객체의
List members
를 변경했을 때?
👉 Member
또는 Team
객체 중 하나(연관관계 주인)만으로 외래키 TEAM_ID fk를 관리해야한다.
양방향 매핑 규칙
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정한다.
- Member 속
Team team
또는 Team 속List members
중 하나를 지정- 연관관계의 주인만이 외래 키를 관리(등록, 수정) 할 수 있다.
- 주인이 아닌쪽은 읽기만 가능하다 (엄청 중요)
- 주인은 mappedBy 속성 사용하면 안된다.
- 주인이 아니면 mappedBy 속성으로 주인 지정한다.
연관관계 주인
으로 설정해야할까❓ 외래 키가 있는 있는 곳을 주인으로 정해라 (갓영한님의 추천)
- 여기서는 Member 속
Team team
이 연관관계의 주인 (진짜 매핑 : 외래키를 등록, 수정할 수 있다)- Team 속
List members
는 연관관계의 하인 (가짜 매핑 : 읽기만 가능하다)
왜 왜래키가 있는 쪽을 주인으로 설정할까 ❓
- 만약 왜래키로 참조받는 쪽을 주인으로 설정한다면,
- Team 객체의 List members 를 update했는데 MEMBER 테이블로 쿼리가 날아간다면 👉 벌써부터 헷갈리기 시작
- 데이터베이스 입장에서 외래키가 있는 테이블이
N
, 아닌 테이블이1
이다
- 이말은 즉, 데이터베이스
N
테이블에 매핑된 객체(Member
)가 객체 연관관계에서 주인이 되어야 한다.
Member
객체 : N 👉@ManyToOne
Team
객체 : 1 👉@OneToMany
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
//회원 저장
Member member = new Member();
member.setUserName("member1");
em.persist(member);
//팀 저장
Team team = new Team();
team.setName("TeamA");
// team의 List members에 member를 집어넣음
// INSERT 쿼리는 나간다.
team.getMembers().add(member);
em.persist(team);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
team.getMembers().add(member);
- MEMBER 테이블의 TEAM_ID fk는 null로 채워진다.
- 연관관계의 주인은 Member의
Team team
인데, Team의List members
로 add했기 때문이다.
위 코드를 고쳐보자
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setUserName("member1");
// 연관관계의 주인인 member로 team을 세팅한다.
member.setTeam(team);
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
member.setTeam(team);
이게 핵심이다
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setUserName("member1");
// 연관관계의 주인인 member로 team을 세팅한다.
member.setTeam(team);
em.persist(member);
System.out.println("em.flush, em.clear");
em.flush();
em.clear();
System.out.println("em.find(Team.class, team.getId());");
Team findTeam = em.find(Team.class, team.getId());
System.out.println("findTeam.getMembers();");
List<Member> members = findTeam.getMembers();
for (Member m : members) {
System.out.println("m.getUserName() = " + m.getUserName());
}
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
em.find(Team.class, team.getId());
시점에 select 쿼리가 나가고
findTeam.getMembers();
시점에서도 select 쿼리가 나간다
- JPA에서
List members
를 사용하는 시점에 select 쿼리를 날린다.
👉team.getMembers().add(member);
를 안해줘도List members
에 값이 세팅된다.
👏그런데,team.getMembers().add(member);
를 안하면 뭔가 객체지향스럽지 않고 문제점이 있다.
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
member.setTeam(team);
team.getMembers().add(member);
- 연관관계 편의 메소드를 생성하자
- 양방향 매핑시에 무한 루프를 조심하자
- 예: toString(), lombok, JSON 생성 라이브러리
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setUserName("member1");
// 연관관계의 주인인 member로 team을 세팅한다.
member.setTeam(team);
em.persist(member);
// team.getMembers().add(member);
// System.out.println("em.flush, em.clear");
// em.flush();
// em.clear();
System.out.println("em.find(Team.class, team.getId());");
Team findTeam = em.find(Team.class, team.getId());
System.out.println("findTeam.getMembers();");
List<Member> members = findTeam.getMembers();
System.out.println("========");
for (Member m : members) {
System.out.println("m.getUserName() = " + m.getUserName());
}
System.out.println("========");
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
em.flush(); em.clear();
하지 않으면 문제가 생긴다.
Member와 Team의 데이터가 영속성 컨택스트의 1차 캐시에만 저장된 상태이다.
Team findTeam = em.find(Team.class, team.getId());
은 1차 캐시에 있는 데이터를 가져온다.
- 데이터베이스가 아닌 메모리에있는 데이터만으로 조회한다. (select 쿼리가 나가지 않는다)
team.getMembers().add(member);
를 해주면
select 쿼리는 나가지 않지만 값을 조회할 수 있다.
- 테스트 케이스 작성할 경우에도
member.setTeam(team);
,team.getMembers().add(member);
처럼 양쪽에 값을 세팅해줘야 한다.
public void setTeam(Team team) {
this.team = team; // member에 team을 세팅
team.getMembers().add(this); // team에 this(member)를 세팅
}
Member 클래스의
setTeam
메소드 안에team.getMembers().add(this);
를 넣어주자
toString()
,lombok
,JSON 생성 라이브러리
에서 문제가 발생한다.
JSON 생성 라이브러리
의 경우 스프링의 Controller에서 Response를 보낼 때,
엔티티 클래스를 사용하면 연관관계에 있는 클래스를 무한 참조하기 때문이다. (그래서 엔티티를Dto
로 변환하여 사용)
@Entity
public class Member {
...
@ManyToOne // member : team = n : 1 (다대일 관계)
@JoinColumn(name = "TEAM_ID") // 객체 연관관계 team 참조와 테이블 연관관계 TEAM_ID 외래키와 매핑
private Team team;
...
@Override
public String toString() {
return "Member{" +
"id=" + id +
", userName='" + userName + '\'' +
", team=" + team +
'}';
}
}
@Entity
public class Team {
...
@OneToMany(mappedBy = "team")
// member : team = n : 1 이므로 OneToMany
// (mappedBy = "team") : Member 클래스의 team이라는 변수와 매핑
private List<Member> members = new ArrayList<>();
...
@Override
public String toString() {
return "Team{" +
"id=" + id +
", name='" + name + '\'' +
", members=" + members +
'}';
}
}
이제 메인함수에서 System.out.println("findTeam="+findTeam);
를 실행하면
Team
의toString()
이members
를 호출하면
👉Member
의toSring()
이team
을 호출하면서 무한루프에 빠져StackOverflowError
발생
- 👏JPA에서의 설계에서 단방향 매핑만으로도 이미 연관관계 매핑은 완료된다
- 처음 설계할 경우, 단방향 매핑으로만 한다.
- 반대쪽으로 조회가 필요할 경우에만 양방향을 추가한다.
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다
- JPQL에서 역방향으로 탐색할 일이 많다
- 👏단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 된다 (테이블에 영향을 주지 않음)
- 연관관계의 주인을 정할 때에는
- 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다
- 👏연관관계의 주인은 외래 키의 위치를 기준으로 정해야한다
즉, N:1관계에서 N에 해당하는 객체에 연관관계를 단방향으로 모두 설정한 뒤에
개발하면서 필요할 때 마다(반대 방향으로의 조회) 양방향 매핑을 추가해주면 된다.