5 분 소요

연관관계 매핑

연관관계가 필요한 이유

객체를 테이블에 맞추어 모델링

연관관계가 없는 객체

Untitled

참조 대신에 외래 키를 그대로 사용 Mybatis 사용시 vo를 이런식으로 설계했었다.

@Entity
public class RMember {
    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String name;

    @Column(name = "TEAM_ID")
    private Long teamId;

    // getter, setter
}
@Entity
public class RTeam {

    @Id @GeneratedValue
    private Long id;

    private String name;

   //getter, setter
}

외래 키 식별자를 직접 다룬다.

//팀 저장
RTeam team = new RTeam();
team.setName("TeamA");
em.persist(team);

//회원 저장
RMember member = new RMember();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);

//조회
RMember findMember = em.find(RMember.class, member.getId());

//연관관계 없음
RTeam findTeam = em.find(RTeam.class, team.getId());

식별자로 다시 조회, 객체 지향적인 방법은 아니다. 물론 테이블의 내용은 정상적으로 들어간다.

Untitled

테이블 중심적 설계시 문제점

객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.

  • 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
  • 객체는 참조를 사용해서 연관된 객체를 찾는다.
  • 테이블과 객체 사이에는 이러한 큰 간격이 있다.

단방향 연관관계

객체 지향 모델링

객체 연관관계 사용

Untitled

객체의 참조와 테이블의 외래 키를 매핑


@Entity
public class RMember {
    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String name;

//    @Column(name = "TEAM_ID")
//    private Long teamId;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private RTeam rTeam;

	//getter, setter
    
}

ORM 매핑

Untitled

연관 관계 저장

//팀 저장
RTeam team = new RTeam();
team.setName("TeamA");
em.persist(team);

//회원 저장
RMember member = new RMember();
member.setName("member1");
member.setrTeam(team);// 단방향 연관관계 설정, 참조 저장
em.persist(member);

영속성 컨텍스트를 초기화 해주면 1차 캐시에서 가져 오지 않고 DB를 직접 조회 한다.

//영속성 컨텍스 초기화
em.flush();
em.clear();

참조로 연관관계 조회 - 객체 그래프 탐색

//조회
RMember findMember = em.find(RMember.class, member.getId());

//연관관계 없음
RTeam findTeam = findMember.getrTeam();

System.out.println("findTeam.getName() = " + findTeam.getName());

@ManyToOne은 FetchType 기본값이 EAGER이기 때문에 조인 쿼리로 한번에 가져온다.

Hibernate: 
    /* insert review.entity.RTeam
        */ insert 
        into
            RTeam
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert review.entity.RMember
        */ insert 
        into
            RMember
            (USERNAME, TEAM_ID, id) 
        values
            (?, ?, ?)
Hibernate: 
    select
        rmember0_.id as id1_11_0_,
        rmember0_.USERNAME as username2_11_0_,
        rmember0_.TEAM_ID as team_id3_11_0_,
        rteam1_.id as id1_12_1_,
        rteam1_.name as name2_12_1_ 
    from
        RMember rmember0_ 
    left outer join
        RTeam rteam1_ 
            on rmember0_.TEAM_ID=rteam1_.id 
    where
        rmember0_.id=?
findTeam.getName() = TeamA

연관관계 수정(fk 수정)

//새로운 팀
RTeam teamB = new RTeam();
teamB.setName("teamB");
em.persist(teamB);

findMember.setrTeam(teamB);
System.out.println("team = " + findMember.getrTeam().getName());

조회 한 member 객체에 새로운 팀을 set 하면 변경감지로 팀이 수정된다.

Hibernate: 
    call next value for hibernate_sequence
team = teamB
Hibernate: 
    /* insert review.entity.RTeam
        */ insert 
        into
            RTeam
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* update
        review.entity.RMember */ update
            RMember 
        set
            USERNAME=?,
            TEAM_ID=? 
        where
            id=?

양방향 연관관계와 연관관계의 주인

양방향 매핑

Untitled

RMember Entity

Member 엔티티는 단방향과 동일하다.


@Entity
public class RMember {
    @Id
    @GeneratedValue
    private Long id;

    @Column(name = "USERNAME")
    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private RTeam rTeam;
//getter, setter
}

RTeam Entity

Team 엔티티는 컬렉션을 추가한다.


@Entity
public class RTeam {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "rTeam")
    private List<RMember> members = new ArrayList<>();
}

반대 방향으로 객체 그래프 탐색

//조회
RTeam findTeam = em.find(RTeam.class, team.getId());
List<RMember> members = findTeam.getMembers();// 역방향 조회

연관관계의 주인과 mappedBy

@OneToMany(mappedBy = “rTeam” 에서 rTeam은 RMember 엔티티의 RTeam 필드 명을 써야하며 rTeam에 의해서 맵핑 되었다는 의미이다.(조회만 가능)

객체와 테이블이 관계를 맺는 차이

  • 객체 연관관계는 2개
    • 회원 → 팀 연관관계 1개(단방향)
    • 팀 → 회원 연관관계 1개(단방행)
  • 테이블 연관관계는 1개
    • 회원 ↔ 팀의 연관관계 1개(양방향)

    Untitled

객체의 양방향 관계

  • 객체의 양방향 관계는 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
  • 객체를 양방향으로 참조 하려면 단방향 연관관계를 2개 만들어야 한다.

Untitled

테이블의 양방향 연관관계

  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
  • MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐
    • 양쪽으로 조인하여 연관관계 정보를 조회 가능
SELECT *
  FROM RMEMBER M
  JOIN RTEAM T ON M.TEAM_ID = T.TEAM_ID;

SELECT *
  FROM TEAM T
  JOIN MEMBER M ON T.TEAM_ID = M.TEAM_Id

외래키 관리

양방향 관계에서는 둘 중 하나로 외래키를 관리 해야한다.

Untitled

연관관계의 주인(Owner)

양방향 매핑 규칙

  • 객체의 두 관계중 하나를 연관관계의 주인으로 지정
  • 연관관계의 주인만이 외래 키를 관리(등록, 수정)
  • 주인이 아닌쪽은 읽기만 가능
  • 주인은 mappedBy 속성 사용하면 안됨
  • 주인이 아니면 mappedBy 속성으로 주인 지정

누구로 주인으로 해야할까?

  • 외래 키가 있는 곳을 주인으로 해야함
  • 예제에서는 Member.team 이 연관관계의 주인

Untitled

양방향 매핑시 가장 많이 하는 실수

//팀 저장
RTeam team = new RTeam();
team.setName("TeamA");
em.persist(team);

//회원 저장
RMember member = new RMember();
member.setUsername("member1");
em.persist(member);
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);

아래와 같이 역방향으로 데이터를 넣어주면 DB에는 null이 들어가 있다. 따라서, 양방향 매핑시 연관관계의 주인에 값을 입력해야 한다. (순수한 객체 관계를 고려하면 항상 양쪽다 값을 입력해야한다.)

Untitled

//팀 저장
RTeam team = new RTeam();
team.setName("TeamA");
em.persist(team);

//회원 저장
RMember member = new RMember();
member.setUsername("member1");

team.getMembers().add(member);
//연관관계의 주인에 값 설정
member.setTeam(team);

em.persist(member);

Untitled

하지만 객체까지 고려한다면, 양쪽 다 관계를 맺어야한다. 즉, 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주어야 순수한 객체 상태에서도 정상적으로 동작한다.

이렇듯 양방향 연관관계는 결국 양쪽 모두를 신경써야한다. 만약, setTeam과 getMembers().add를 각각 호출하면 실수가 발생할 수 있다. 따라서 양쪽 모두의 관계를 맺어주는 것을 하나의 코드처럼 사용하는 것이 안전하다.

메소드의 명칭도 setter의 명칭을 사용 하지 않고 changeTeam와 같이 의미를 알 수 있는 명명법을 사용한다.

RMember.class

public void changeTeam(RTeam rTeam) {
        this.rTeam = rTeam;
        rTeam.getMembers().add(this);//나 자신
}

//팀 저장
RTeam team = new RTeam();
team.setName("TeamA");
em.persist(team);

//회원 저장
RMember member = new RMember();
member.setUsername("member1");
member.changeTeam(team);  //연관관계 메소드 member 중심으로 값 세팅
em.persist(member);

//영속성 컨텍스 초기화
//em.flush();
//em.clear();

RTeam findTeam = em.find(RTeam.class, team.getId());//1차 캐시
List<RMember> members = findTeam.getMembers();

System.out.println("================");
for (RMember m : members) {
    System.out.println("m = " + m.getUsername());
}
System.out.println("================");
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
================
m = member1
================
Hibernate: 
    /* insert review.entity.RTeam
        */ insert 
        into
            RTeam
            (name, id) 
        values
            (?, ?)
Hibernate: 
    /* insert review.entity.RMember
        */ insert 
        into
            RMember
            (TEAM_ID, USERNAME, id) 
        values
            (?, ?, ?)

이렇듯 아래와 같이 team을 조회한 후 member를 조회하면 DB에서 조회 하지 않고 1차 캐시에서 조회 하기때문에 쿼리를 질의 하지 않는다.

member.changeTeam(team);  
member.changeTeam(team2);  

위와 같이 연속적으로 changeTeam을 호출한 이후 team에서 멤버를 조회하면 member1가 여전히 조회된다. team2로 변경할 때 team1과의 관계를 제거하지 않았기 때문이다.

public void changeTeam(RTeam rTeam) {
    if (this.rTeam != null) { // 기존에 이미 팀이 존재한다면
        this.rTeam.getMembers().remove(this); // 관계를 끊는다.
    }
    this.rTeam = rTeam;
    rTeam.getMembers().add(this);//나 자신
}

따라서 위와 같이 기존 팀과의 관계를 제거하는 코드를 추가해야 정상적으로 동작한다.

출처

인프런 김영한님 자바 ORM 표준 JPA 프로그래밍 - 기본편 https://joanne.tistory.com/220

태그:

카테고리:

업데이트: