연관 관계

현시기얌·2021년 11월 29일
0

JPA

목록 보기
8/14

1. 단방향 연관관계

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String username;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;

    public Member(String username) {
        this.username = username;
    }

    public static Member of(String username) {
        return new Member(username);
    }

    public void addTeam(Team team) {
        this.team = team;
    }
}

한 팀에 여러 멤버가 들어갈 수 있으므로 Member 와 Team은 N : 1 관계이다.
따라서 Member에 @ManyToOne 애노테이션을 붙이고 Team 과 team_id(FK)를 연관관계 매핑하였다.

@ManyToOne(fetch = FetchType.LAZY) 를 사용하면 Member 객체만 조회해오고 Team 객체의 정보가 필요하면 그 때 DB에서 Team 객체를 조회해온다.

@ManyToOne(fetch = FetchType.EAGER)

            final Team team = Team.of("TeamA");
            em.persist(team);

            final Member member = Member.of("member1");
            member.addTeam(team);
            em.persist(member);

            em.flush();
            em.clear();

            final Member findMember = em.find(Member.class, member.getId());

실행 결과

@ManyToOne(fetch = FectchType.LAZY)

            final Team team = Team.of("TeamA");
            em.persist(team);

            final Member member = Member.of("member1");
            member.addTeam(team);
            em.persist(member);

            em.flush();
            em.clear();

            final Member findMember = em.find(Member.class, member.getId());

실행 결과

아직 Team 객체의 정보가 필요 없기 때문에 DB에서 Team을 조회해 오지 않았다.

final Team team = Team.of("TeamA");
            em.persist(team);

            final Member member = Member.of("member1");
            member.addTeam(team);
            em.persist(member);

            em.flush();
            em.clear();

            final Member findMember = em.find(Member.class, member.getId());

            System.out.println("===================================================");

            final Team findTeam = findMember.getTeam();

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

실행 결과

Team의 Name 정보가 필요할 때 DB에서 Team 객체의 정보를 가져왔다.

2. 양방향 연관관계와 연관관계 주인

양방향 연관관계에서 DB 테이블은 FK와 PK로 조인을 하기 때문에 변화가 없다.
즉 테이블의 연관관계는 FK 하나로 양방향 연관관계가 가능하다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public Team(String name) {
        this.name = name;
    }

    public static Team of(String name) {
        return new Team(name);

    }

}

하지만 객체의 연관관계는 Member에는 Team객체 Team 객체에는 Member 객체를 넣어줘야한다.

연관 관계의 주인과 mappedBy

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

  • 객체 연관관계 = 2개
    • 회원 -> 팀 연관관계 1개(단방향)
    • 팀 -> 회원 연관관계 1개(단방향)
  • 테이블 연관관계 = 1개
    • 회원 <-> 팀의 연관관계 1개(양방향)

객체의 양방향 관계

  • 객체의 양뱡향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다.
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
  • A -> B(a.getB())
class A {
    B b;
}
  • B -> A(b.getA())
class B {
    A a;
}

테이블의 양방향 연관관계

  • 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
  • member.team_id 외래 키 하나로 양방향 연관관계를 가진다. (양쪽으로 조인할 수 있다.)
SELECT * 
FROM MEBMER 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

둘 중 하나로 외래 키를 관리해야 한다.

과연 그럼 Member의 team이 업데이트 되었을 때 테이블의 TEAM_ID가 업데이트 되어야 할지
Team의 members가 업데이트 되었을 때 테이블의 TEAM_ID가 업데이트 되어야 할지 딜레마가 온다.
따라서 둘 중 하나로 외래 키를 관리해야 한다.

연관 관계 주인

양방향 매핑 규칙

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

누구를 주인으로 ?

  • 외래 키가 있는 곳을 주인으로 정해라 ( N : 1 쪽에서 N 쪽(@ManyToOne)이 연관관계의 주인)
  • 여기서는 Member.team이 연관관게의 주인

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

            final Member member = Member.of("member1");
            em.persist(member);


            final Team team = Team.of("TeamA");
            //역방향(주인이 아닌 방향)만 연관관계 설정
            team.getMembers().add(member);
            em.persist(team);

실행 결과

Hibernate: 
    /* insert hellojpa.domain.member.Member
        */ insert 
        into
            Member
            (id, team_id, username) 
        values
            (null, ?, ?)
Hibernate: 
    /* insert hellojpa.domain.team.Team
        */ insert 
        into
            Team
            (id, name) 
        values
            (null, ?)

Insert 쿼리는 2번 나왔지만 역방향에서 연관관계를 설정했다. (Team의 members)
Team의 members는 mappedBy로 읽기 전용이기 때문에 Insert, Update 쿼리에 반영되지 못한다.

            final Team team = Team.of("TeamA");
            em.persist(team);

            final Member member = Member.of("member1");
            member.addTeam(team);
            em.persist(member);

실행 결과

따라서 위와 같이 양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
하지만 순수한 객체 관계를 고려하면 양쪽다 값을 입력하는 것이 제일 바람직하다.

            final Team team = Team.of("TeamA");
            em.persist(team);

            final Member member = Member.of("member1");
            member.addTeam(team);
            team.getMembers().add(member);
            em.persist(member);

양쪽 다 값을 입력해야 하는 이유

만약 트랜잭션 커밋이 일어나지 않았는데 team.getMember()로 Team에서 members들을 찾는다면 1차캐시에 저장된 정보를 가져오기 때문에 값을 찾을 수 없다.

연관관계 편의 메소드

    public void addTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }

연관관계를 양쪽에 다 설정하다보면 종종 실수할때가 있다.
때문에 연관 관계를 맺을 때 한번에 양쪽을 둘 다 맺으면 편리하다.

양방향 매핑 정리

  • 단방향 매핑만으로도 이미 연관관계 매핑은 완료
  • 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
  • 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 된다.(테이블에 영향을 주지 않음)
  • 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.
profile
현시깁니다

0개의 댓글