본문 바로가기
프로그래밍/JPA

05장. 연관관계 매핑기초

by 직장인 투자자 2022. 1. 22.

개요

  1. 방향(direction): 단방향, 양방향
  2. 다중성(multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)
  3. 연관관계의 주인(owner): 객체 관계를 주도하는 주인을 지정

 

 

단방향 연관관계

객체 연관관계

  • Member 객체는 Member.team 필드(멤버 변수)로 Team 객체와 연관관계를 맺는다.
  • Member 객체와 Team 객체는 단방향 관계다.
  • Member는 Member.team 필드를 통해서 Team을 알 수 있지만, 반대로 Team은 Member를 알 수 없다.
  • Member member = repository.findOne(id); Team team = member.getTeam();

테이블 연관관계

  • 테이블은 외래 키(FK)로 연관관계를 맺음
  • MEMBER 테이블은 TEAM_ID 외래 키로 TEAM 테이블과 연관관계를 맺는다. MEMBER 테이블과 TEAM테이블은 양방향 관계다.
  • MEMBER 테이블의 TEAM_ID 외래 키를 통해서 MEMBER와 TEAM을 JOIN 할 수 있고, 반대로 TEAM과 MEMBER 도 JOIN 할 수 있다.
  • SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.ID; SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID;

객체 연관관계와 테이블 연관관계

  • 객체는 참조(주소)로 연관관계를 맺고, 테이블은 외래 키로 연관관계를 맺는다.
  • 참조를 사용하는 객체의 연관관계는 단방향이고, 외래 키를 사용하는 테이블의 연관관계는 양방향이다.
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

객체 관계 매핑

  • 객체 연관관계: Member 객체의 Member.team 필드 사용
  • 테이블 연관관계: Member 테이블의 MEMBER.TEAM_ID 외래 키 칼럼을 사용
@Getter
@Setter
@Entity
public class Member {

  @Id
  @Column(name = "MEMBER_ID") 
  private String id;

  private String username;

  // 연관관계 매핑
  @ManyToOne 
  @JoinColumn(name="TEAM_ID") 
  private Team team;

  // 연관관계 설정
  public void setTeam(Team team) {
    this.team = team;
  }
}
@Getter
@Setter
@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;
}

@JoinColumn

  • 외래 키를 매핑할때 사용한다.
  • 이 어노테이션은 생략할 수 있다.
  • 이 어노테이션을 생략하면 외래키를 찾을 때 기본 전략을 사용한다.위 예제에서 생략 시 team_TEAM_ID 외래 키를 사용한다.
  • 기본전략: 필드명 + _ + 참조하는 테이블의 칼럼명

@ManyToOne

  • 다대일(N:1) 관계를 의미한다.

targetEntity 속성 예시

 @OneToMany
 private List<Member> members; // 제네릭으로 타입 정보를 알 수 있다.

 @OneToMany(targetEntity=Member.class)
 private List members; // 제네릭이 없으면 타입 정보를 알 수 없다.
  • 연관관계 매핑 시 필수로 사용해야 한다.(다중성을 나타냄)

연관관계 사용

저장

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.

public void testSave() {

  // 팀1 저장
  Team team1 = new Team("team1", "팀1");
  em.persist(team1);

  // 회원1 저장
  Member member1 = new Member("member1", "회원1");
  member1.setTeam(team1);  // 연관관계 설정 member1 -> team1
  em.persist(member1);

  // 회원2 저장
  Member member2 = new Member("member2", "회원2");
  member2.setTeam(team1);  // 연관관계 설정 member2 -> team1
  em.persist(member2);  
}
INSERT INTO TEAM(TEAM_ID, NAME) VALUES ('team1', '팀1')
INSERT INTO MEMBER(MEMBER_ID, NAME, TEAM_ID) VALUES ('member1', '회원1', 'team1')
INSERT INTO MEMBER(MEMBER_ID, NAME, TEAM_ID) VALUES ('member2', '회원2', 'team1')

조회

  • 객체 그래프 탐색
    • 객체 연관관계를 사용한 조회, 객체를 통해 연관된 엔티티를 조회하는 것이다.
  • 객체지향 쿼리(JPQL) 사용
    '팀 1' FROM MEMBER MEMBER
    INNER JOIN TEAM TEAM1_ ON MEMBER.TEAM_ID = TEAM1_.ID
    WHERE TEAM1_.NAME = '팀1'

수정

private static void updateRelation(EntityManager em) {

  // 새로운 팀2
  Team team2 = new Team("team2", "팀1");
  em.persist(team2);

  // 회원1에 새로운 팀2 설정
  Member member1 = em.find(Member.class, "member1");
  member1.setTeam(team2);
}
UPDATE MEMBER
SET TEAM_ID = 'team2', ...
WHERE ID = 'member1'
  • 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다.
  • 연관관계를 수정할 때도 참조하는 대상만 변경하면 JPA가 자동으로 처리한다.

 

 

연관관계 제거

private static void deleteRelation(EntityManager em) {
  Member member1 = em.find(Member.class, "member1");
  member1.setTeam(null);    // 연관관계 제거
}
UPDATE MEMBER
SET TEAM_ID = null, ...
WHERE ID = 'member1'

연관된 엔티티 삭제

  • 연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
member1.setTeam(null);
member2.setTeam(null);
em.remove(team1);

양방향 연관관계

@Getter
@Setter
@Entity
public class Member {

  @Id
  @Column(name = "MEMBER_ID") 
  private String id;

  private String username;

  // 연관관계 매핑
  @ManyToOne 
  @JoinColumn(name="TEAM_ID") 
  private Team team;

  // 연관관계 설정
  public void setTeam(Team team) {
    this.team = team;
  }
}
@Getter
@Setter
@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

      @OneToMany(mappedBy = "team")
      private List<Member> members = Lists.newArrayList();
}
  • mappedBy: 양방향 매핑일 때 사용, 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
public void biDirection() {
      Team team = em.find(Team.class, "team1");
      List<Member> members = team.getMembers();    // 팀 -> 회원, 객체 그래프 탐색
}

연관관계의 주인

  • 객체 연관관계
    • 회원 -> 팀 연관관계 1개(단방향)
    • 팀 -> 회원 연관관계 1개(단방향)
  • 테이블 연관관계
    • 회원 <-> 팀 연관관계 1개(양방향)
  • 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘의 차이가 발생한다.
  • 연관관계의 주인 : 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리하는 것이다.

양방향 매핑의 규칙: 연관관계의 주인

  • 두 객체 연관관계 중 연관관계의 주인을 정해서 테이블의 외래 키(FK)를 관리해야 한다.
    • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제) 할 수 있다.
    • 주인이 아닌 쪽은 읽기만 할 수 있다.
  • 연관관계 주인은 mappedBy 속성으로 지정
    • 주인은 mappedBy 속성을 사용하지 않는다.
    • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

연관관계의 주인은 외래 키가 있는 곳

  • DB 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.

양방향 연관관계의 주의점

  • 양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
public void testSaveNonOwner() {

  // 회원1 저장
  Member member1 = new Member("member1", "회원1");
  em.persist(member1);

  // 회원2 저장
  Member member2 = new Member("member2", "회원2");
  em.persist(member2);  

  // 팀1 저장
  Team team1 = new Team("team1", "팀1");
  // 주인이 아닌 곳만 연관관계 설정
  team1.getMembers().add(member1);
  team1.getMembers().add(member2);

  em.persist(team1);
}

해당 코드를 수행하면 MEMBER 테이블의 TEAM_ID는 null로 저장되어 있다. 연관관계의 주인이 아니기 때문이다.

순수한 객체까지 고려한 양방향 연관관계

객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.

public void test순수한객체_양방향() {
  // 팀1 저장
  Team team1 = new Team("team1", "팀1");
  Member member1 = new Member("member1", "회원1");
  Member member2 = new Member("member2", "회원2");

  member1.setTeam(team1);  // 연관관계 설정 member1 -> team1
  member2.setTeam(team1);  // 연관관계 설정 member2 -> team1

  log.info("member size: {}", team1.getMember().size());  // member size: 0

  team1.getMembers().add(member1);    // 연관관계 설정 team1 -> member1
  team1.getMembers().add(member2);  // 연관관계 설정 team1 -> member2

  log.info("member size: {}", team1.getMember().size());  // member size: 2
}

Member와 Team에 양방향 관계를 설정했으면, 순수한 객체에서도 관계가 유지되어야 한다.

객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.

연관관계 편의 메서드

@Getter
@Setter
@Entity
public class Member {

  @Id
  @Column(name = "MEMBER_ID") 
  private String id;

  private String username;

  // 연관관계 매핑
  @ManyToOne 
  @JoinColumn(name="TEAM_ID") 
  private Team team;

  // 연관관계 설정
  public void setTeam(Team team) {

    // 기존 팀과 관계를 제거
    if (this.team != null) {
        this.team.getMembers().remove(this);
    }
    this.team = team;
    team.getMembers().add(this);
  }
}
  • 연관관계를 변경할 때는 기존 Team이 있으면 기존 Team과 Member의 연관관계를 삭제하는 코드를 추가해야 한다.
  • this.team.getMembers(). remove(this); 코드가 없어도 외래 키를 변경하는 데는 문제가 없고, 새로운 영속성 콘텍스트에서도 변경된 팀이 조회된다.
  • 문제는 관계를 변경하고 영속성 콘텍스트가 아직 살아있는 상태에서는 이전에 관계를 유지하고 있던 팀이 조회된다.

정리

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
  • 양방향 매핑 시에는 무한 루프에 빠지지 않게 조심해야 한다.
반응형

'프로그래밍 > JPA' 카테고리의 다른 글

04장. Entity Mapping  (6) 2022.01.18
03장. 영속성 관리  (2) 2022.01.14
02. JPA 시작  (10) 2022.01.11
01. JPA 소개  (4) 2022.01.08

댓글