공부/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에서 역방향으로 탐색할 일이 많음
  • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)