JPA

[JPA] 7. 연관관계

inuma 2021. 1. 6. 10:27
프로젝트 git url github.com/rtef23/JpaStudy
필요 1. docker
2. docker-compose
프로젝트 정보 mysql : 5.7 - docker image
java : 1.8

 

용어 정리

  • 방향(Direction) : 단방향, 양방향
  • 다중성(Multiplicity) : 다대일(N : 1), 일대다(1 : N), 일대일(1 : 1), 다대다(N : M)
  • 연관관계의 주인(Owner) : 객체 양방향 연관관계는 관리 주인이 필요

 

예시

  • Member와 Team의 관계가 N : 1의 경우

  • insert 예시
Team team = new Team();
team.setName("team-1");

entityManager.persist(team);

Member member = new Member();
member.setUsername("user-name-1");
member.setTeamId(team.getId());

entityManager.persist(member);
  • select 예시
Team findTeam = entityManager.find(Team.class, 1L);

Member findMember = entityManager.persist(Team.class, findTeam.getTeamId());
  • 저장시 각 엔티티마다 별도로 저장을 하거나, 조회시 각 엔티티의 식별자로 조회를 해야한다.
  • 객체는 참조를 사용를 사용하여 연관된 객체를 찾고 테이블은 외래키로 연관된 테이블을 찾는다는 점에서 차이점이 존재
    • => 객체 지향스럽지 않게 된다.

 

개선 - 단방향 연관관계

@Entity
public class Member {
  @ManyToOne
  @JoinColumn(name = "team_id")
  private Team team;
  
  ...
}
  • select 예시
Member findMember = entityManager.find(Member.class, 1L);

Team findTeam = findMember.getTeam();
//@ManyToOne, @JoinColumn 어노테이션을 통해 team_id로 조인된 Team instance를 가져올 수 있게 된다.
  • update 예시
Member member = entityManager.find(Member.class, 1L);
Team newTeam = entityManager.find(Team.class, 2L);

member.setTeam(newTeam);

entityTransaction.commit();
//DB에서 Member 테이블에 team_id 값이 업데이트 되게 된다.
  • @ManyToOne annotation
    • Member 클래스와 다음의 필드(Team)가 N : 1 관계임을 나타냄
  • @JoinColumn
    • Member 클래스와 Team 클래스는 team_id 컬럼을 통해서 조인된다는 것을 나타냄

개선 - 양방향 연관관계

  • Team을 통해서도 Member를 참조할 수도 있고 Member를 통해서도 Team을 참조할 수 있다.
public class Team {
  @OneToMany(mappedBy = "team")
  private List<Member> members = new ArrayList<>();
  //mappedBy에는 연관 관계의 주인에서 참조한 필드명을 입력해야 한다.
  
  ...
}

public class Member {
  @ManyToOne
  private Team team;

  ...
}
  • 객체에서의 연관 관계
    • Member -> Team ; 연관관계 1개(단방향)
    • Team -> Member ; 연관관계 1개(단방향)
    • 객체에서의 양방향 관계는 서로 다른 단방향 관계 2개이다.
Member member = entityManager.find(Member.class, 1L);

member.getTeam();

Team team = entityManager.find(Team.class, 1L);

team.getMembers();

 

  • 테이블에서의 연관 관계
    • Member <-> Team ; 연관관계 1개(양방향), 외래키를 통해서 연관 관계 관리
//MEMBER 테이블을 기준으로 TEAM을 조인하여 조회하는 쿼리
SELECT *
  FROM MEMBER M
 INNER JOIN TEAM T
    ON M.team_id = T.team_id
    
    
//TEAM 테이블을 기준으로 MEMBER를 조인하여 조회하는 쿼리
SELECT *
  FROM TEAM T
 INNER JOIN MEMBER M
    ON T.team_id = M.team_id

 

  • Member의 Team을 바꿔야 할 때
    • Member 객체의 Team을 바꿔야 할지 아니면 Team에서 Member 리스트를 바꿔야 할지?
  • 연관관계의 주인(Owner)
    • 객체의 두 단방향 관계중 하나를 연관관계의 주인으로 지정
    • 연관 관계의 주인만이 외래키를 관리(등록, 수정)
    • 주인이 아닌쪽은 읽기만 가능
    • 주인이 아닌 쪽에서 mappedBy 속성으로 주인을 지정한다.
  • 주인(Owner) 선정 가이드
    • 테이블에서 외래키를 가지고 있는 클래스를 기준으로 주인 선정하는 것을 추천
      • 현재 예시에서는 Member 테이블에 team_id인 외래키를 가지고 있기 때문에 Member가 연관관계의 주인으로 선정
  • 양방향 매핑시 가장 많이 하는 실수
    • 연관관계의 주인에서만 외래키를 등록, 수정 하도록 해야한다.
    • 문제
Member member = new Member();
member.setUsername("user-1");

entityManager.persist(member);

Team team = new Team();
team.setName("team-1");
team.getMembers().add(member);

entityManager.persist(team);

/*
  SELECT * FROM MEMBER;
  | MEMBER                  |
  | id | username | team_id |
  |----|----------|---------|
  |  1 | user-1   | null    |
  쿼리 실행시 insert로 들어간 member 데이터의 team_id는 null로 들어가 있게 된다.
  * 연관관계의 주인은 Member이다.
  JPA에서 저장시 @OneToMany의 경우, persist API 실행시 참조하지 않는다.
*/
    • 개선
Team team = new Team();
team.setName("team-1");

entityManager.persist(team);

Member member = new Member();
member.setUsername("user-1");
member.setTeam(team);

entityManager.persist(member);

/*
  SELECT * FROM MEMBER;
  | MEMBER                  |
  | id | username | team_id |
  |----|----------|---------|
  |  1 | user-1   | 1       |
  * 연관관계의 주인은 Member이다.
  * 문제 없이 실행은 된다.
*/

      • 양방향 연관관계의 경우 양쪽의 객체에 추가 로직을 넣는 것이 좋다.
Team team = new Team();
team.setName("team-1");

entityManager.persist(team);

Member member = new Member();
member.setUsername("user-1");
member.setTeam(team); //**

entityManager.persist(member);

team.getMembers().add(member); //**
/*
  team 객체는 영속성 컨텍스트의 1차 캐시에 있기 때문에 
  다시 DB를 갔다오지 않는 경우(flush(), clear() api를 사용하지 않는 경우),
  조회했던 Member 리스트에는 추가한 Member가 존재하지 않기 때문에
  위 코드를 추가해주는 것이 좋다.
*/

/*
  SELECT * FROM MEMBER;
  | MEMBER                  |
  | id | username | team_id |
  |----|----------|---------|
  |  1 | user-1   | 1       |
  * 연관관계의 주인은 Member이다.
*/

    • setTeam() 메소드에 양쪽에 객체를 추가하는 로직을 추가하면 좋다.
      • 둘 중에 한 곳에서 처리할 수 있도록 하는 것이 좋다.
      • 무한 루프를 조심해야한다.([example] toString(), lombok, JSON 라이브러리 ....)
        • Controller에서는 엔티티를 반환하지 않도록 하는 것이 좋다. 대신 DTO를 별도로 만들어 리턴하는 것이 좋다.
public class Member {
  private Team team;
  
  public void setTeam(Team team){
    this.team = team;
  }
  
  public void changeTeam(Team team){
    this.team = team;
    team.getMembers().add(this); //**
  }

  ...
}


public class Team {
  private List<Member> member;
  
  public void addMember(Member member){
    this.member.add(member);
    member.setTeam(this);
  }
  
  ...
}

 

  • 양방향 매핑은 필요한 경우에만 추가하는 것을 추천