5장. 연관관계 매핑 기초

  • object는 reference로 연관관계
  • table은 foreign key로 연관관계
  • 서로 다른 둘을 mapping 하는데에는 다음 세 가지가 핵심 키워드
    • Direction: 참조하는 방향. 한 쪽만 참조하면 단방향, 서로 참조시 양방향(reference를 통한 연관관계은 항상 단방향임. 서로 참조시에도 unidrectional 연관관계가 두개인 것임). table은 항상 양방향
    • Multiplicity: 1:1, 1:N, N:1, N:M
    • Owner: 양방향 연관관계al 만들 경우 owner 설정해야 함




5.1 단방향

  • member & team
  • member는 하나의 team 에만 소속 가능
    • ∴ member와 team 은 N:1 관계

  • object 연관관계
    • member object는 Member.team field로 team object와 연관관계
    • member와 team은 단방향
    • object는 reference를 통해 연관관계 탐색 -> 객체 그래프 탐색
    • member 는 member.team 으로 team 을 조회할 수 있지만 team 에선 불가능함
    • reference를 통한 연관관계은 항상 단방향
  • table 연관관계
    • member table은 team_id foreign key로 team table 과 연관관계
    • member table과 team table 은 양방향
    • team_id 키로 join해서 member 와 team, team 과 member 조회 모두 가능



5.1.1 object 연관관계 mapping

코드
// code
@Entity
public class Membnber {
  @Id
  private String id;

  private String username;

  // 연관관계 mapping
  @ManyToOne
  @JoinColumn(name = "team_id")
  private Team team;

  // 연관관계 설정
  public void setTeam(Team team) {
    this.team = team;
  }
  ...
}

...

@Entity
public class Team {
  @Id
  private Stirng id;

  private String name;
  ...
}

  • object 연관관계: member object의 Member.team field 사용
  • table 연관관계: member table 의 member.team_id fk 사용
  • Member.team과 member.team_id 를 mapping 하는 것이 연관관계 mapping


@JoinColumn

속성 기능 기본값
name mapping 할 fk 이름 {field 명}_{참조하는 table의 pk column 명}
referencedColumnName fk가 참조하는 대상 table의 column 명 참조하는 table의 pk column 명
foreignKey(DDL) fk constraints 직접 지정 가능. table 생성시에만 사용  
unique @Column 속성과 동일  
nullable @Column 속성과 동일  
insertable @Column 속성과 동일  
updatable @Column 속성과 동일  
columnDefinition @Column 속성과 동일  
table @Column 속성과 동일  

@JoinColumn 생략
생략시 default strategy 사용하여 fk 찾음
{field 명}_{참조하는 table의 pk column 명}=fk 이름
따라서 위 예제 같은 경우 fk는 알아서 team_id가 됨


@ManyToOne

  • N:1 연관관계에서 사용
속성 기능 기본값
optional false 설정시 연관된 entity가 항상 있어야 함(non nullable) true
fetch global fetch strategy 설정 @ManyToOne: FetchType.EAGER
@OneToMany: FetchType.LAZY
cascade 영속성 전이 기능을 사용  
targetEntity 연관된 entity의 타입 정보를 설정. 거의 안씀. collection 사용해도 generic으로 타입 정보 알 수 있음  
  • targetEntity 속성 사용 예시
코드
@OneToMany
private List<Member> members;   // generic으로 타입 정보 알 수 있음

@OneToMany(targetEntity=Member.class)
private List members            // genreic 없으면 타입 정보 알 수 없음. 그래서 targetEntity로 타입 정보 설정




5.2 연관관계 사용

저장

코드
// code
Team team1 = new Team("team1", "Alpha");
em.persist(team1);    // !! jpa 에서 entity 저장 시 연관된 모든 entity 는 영속 상태여야 함 !!

Member member1 = new Member("member1", "Alice");
member1.setTeam(team1);
em.persist(member1);

Member member2 = new Member("member2", "Bob");
member2.setTeam(team1);
em.persist(member2);

sql
# sql
INSERT INTO TEAM (id, name) VALUES ('team1', 'Alpha');
INSERT INTO MEMBER(id, username, team_id) VALUES ('member1', 'Alice');
INSERT INTO MEMBER(id, username, team_id) VALUES ('member2', 'Bob');



5.2.2 조회

  • 연관관계sihp 있는 entity 조회 방법은 크게 다음 2가지임
    1. 객체 그래프 탐색
    2. JPQL(객체지향 쿼리) 사용
  • 객체 그래프 탐색
    • 객체를 통해 연관된 entity를 조회하는 방법
    • 위 예제와 같은 경우 다음과 같이 member와 연관된 team 조회

코드
    Member member = em.find(Member.class, "member1");
    Team team = member.getTeam();   // 객체 그래프 탐색
    

</details>

  • JPQL
    • 특정 team 에 소속된 member 만 조회하려면 member와 연관된 team entity 를 검색 조건으로 사용
      • SQL은 연관 table 을 join 해서 검색조건 사용하면 된다
      • JPQL도 join 을 지원
코드
    // code
    private static void queryLogicJoin(EntityManager em) {
      // member가 team 과 연관관계 가지고 있는 field인 m.team 을 통해서 member 와 team join
      // :teamName 처럼 :로 시작하는 것은 parameter binding 문법
      String jpql = "select m from Member m join m.team t where " + "t.name=:teamName";

      List<Member> resultList = em.createQuery(jpql, Member.class)
        .setParameter("teamName", "Alpha")
        .getResultList();
    }
    

</details>

<details>
sql
    # sql
    SELECT m.*
    FROM member m
        INNER JOIN team t
        ON m.team_id = t.id
    WHERE t.name = 'Alpha'
    

</details>



5.2.3 수정

  • 일반적인 entity 수정과 마찬가지로 entity 가 참조하는 대상만 변경해 두면 이후 tarnsaction commit 시 flush 일어나면서 변경 감지를 통해 알아서 처리됨

코드
    // code
    Team team2 = new Team("team2", "Beta");
    em.persist(team2);

    Member member = em.find(Member.class, "member1");
    member.setTeam(team2);
    

</details>

<details>
sql
    # sql
    UPDATE member
    SET team_id = 'team2', ...
    WHERE id = 'member1'
    

</details>



5.2.4 연관관계 제거

코드
// code
Member member1 = em.find(Member.class, "member1");
member1.setTeam(null);    // 연관관계 제거

sql
# sql
UPDATE member
SET team_id = null
WHERE id = 'member1'



5.2.5 연관된 entity 삭제

  • 연관된 entity 삭제하려면 먼저 기존에 있던 연관관계를 제거해야 함
    • 서순 틀리면 fk constraint 로 인해 DB error 발생

코드
    // 위 예제에서의 team Alpha 삭제하려면 먼저 소속 member에서의 reference를 모두 지워서 연관관계 끊어야 함
    member1.setTeam(null);
    member2.setTeam(null);
    em.remove(team);
    

</details>


5.3 양방향 연관관계

  • member -> team (Member.team) 은 N:1
  • team -> member (Team.members) 는 1:N
    • 1:N 관계는 여러 건과 연관관계 맺을 수 있으므로 collection 사용해야 함
    • Team.members 를 List collection 으로 추가(List 포함 Collection, Set, Map 등 다양하게 지원)



5.3.1 양방향 연관관계 mapping

코드
@Entity
public class Member {
  @Id
  private Stirng id;

  private String username;

  @ManyToOne
  @joinColumn(name = "team_id")
  private Team team;

  // 연관관계 설정
  public void setTeam(Team team) {
    this.team = team;
  }
  ...
}

...

@Entity
public class Team {
  @Id
  private String id;

  private String name;

  // 1:N 연관관계 설정
  @OneToMany(mappedBy = "team")
  private List<Member> members = new ArrayList<Member>();
  ...
}

  • @OneToMany 의 mappedBy 속성은 양방향 mapping 일 때 사용
    • 반대쪽 mapping 의 field 이름을 값으로 하면 됨




5.4 연관관계의 owner

  • entity의 연관관계를 양방향으로 설정 시 객체 참조는 둘, foregin key는 하나
    • 차이가 발생
    • ∴ owner를 정해야 함



5.4.1 양방향 mapping의 규칙: 연관관계의 owner

  • bidriection 연관관계 mapping 시 두 연관관계 중 하나는 owner로 정해야 함
  • owner 만이 DB 연관관계과 mapping 되고 foriegn key를 관리(등록, 수정, 삭제) 가능
    • owner 아닌 쪽은 read only
  • 이 때 연관관계 owner 정해주는게 mappedBy 속성
  • owner 아닌 쪽에서 owner 를 mappedBy 속성 값으로 지정해주면 됨

  • members -> team
코드
@Entity
class Member {
  @ManyToOne
  @JoinColumn(name = "team_id")
  private Team team;
  ...
}

  • team -> members
코드
@Entity
class Team{
  @OneToMany
  private List<Member> members = new ArrayList<Member>();
  ...
}

  • team entitiy의 Team.members 를 owner로 설정하는 경우
    • 다른 테이블(member)에 있는 foreign key를 관리해야 함
  • member entity의 Member.team 을 owner로 설정하는 경우
    • 자기 테이블에 있는 foreign key 관리하면 됨
  • 따라서 ownere는 foreign key가 있는 곳으로 설정해야 함
    • 위 예시 같은 경우에는 Member.team 을 owner로 설정

DB table의 N:1, 1:N 관계에서는 항상 N 쪽이 foreign key 가짐 >br/> 따라서 @ManyToOne 은 항상 owner side 이기 때문에 mappedBy 속성이 없음




5.5 양방향 연관관계 저장

코드
// team1 저장
Team team1 = new Team("team1", "Alpha");
em.persist(team1);

// member1 저장
Member member1 = new Member("member1", "Alice");
// 연관관계 설정: Member(Alice) -> Team(Alpha)
member1.setTeam(team1);
em.persist(member1);

// member2 저장
Member member2 = new Member("member2", "Bob");
// 연관관계 설정: Member(Bob) -> Team(Alpha)
member2.setTeam(team1);
em.persist(member2);

id username team_id
member1 Alice team1
member2 Bob team1
  • 위와 같이 owner side인 Member.team에 연관관계인 Team 설정해주면 member table의 foreign key가 알아서 설정됨




5.6 양방향 연관관계 주의점

  • 양방향 연관관계 설정시 non-owning side 에만 값 넣으면 제대로 반영이 안됨
    • owner side 에서 mapping된 foreign key 관리하기 때문에 owner side에서 값 설정 안해주면 DB에 안들어감



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

  • 다만 객체 관점에서 보았을 때 양쪽 방향 모두 값 입력해 주는 것이 안전
  • DB에는 제대로 저장되기 때문에 해당 persistence context 종료 후 새로 만들어 조회하면 정상적으로 사용
  • 하지만 그 전까지 순수한 객체 상태일 때는 값 넣어주지 않은 쪽에서는 연관 객체 접근이 불가능
    • 의도한 대로 동작 X
  • ∴ 아래와 같이 양 쪽에 모두 값 넣어주는 것이 좋음
코드
// team1 저장
Team team1 = new Team("team1", "Alpha");
em.persist(team1);

// member1 저장
Member member1 = new Member("member1", "Alice");
// 연관관계 설정: Member(Alice) -> Team(Alpha)
member1.setTeam(team1);
// 연관관계 설정: Team(Alpha) -> Member(Alice)
team1.getMembers().add(member1);
em.persist(member1);

// member2 저장
Member member2 = new Member("member2", "Bob");
// 연관관계 설정: Member(Bob) -> Team(Alpha)
member2.setTeam(team1);
// 연관관계 설정: Team(Alpha) -> Member(Bob)
team1.getMembers().add(member2);
em.persist(member2);




5.6.2 연관관계 편의 method

  • 위 예시처럼 양방향 연관관계 에서는 양쪽 모두 값 넣어줘야 함
    • 직접 넣는 방식은 실수할 확률이 높음
  • ∴ 한번에 양 쪽 다 넣어주는 method 작성해서 쓰는게 좋음
    • 이를 연관관계 편의 method라 부름
코드
@Entity
public class Member {
  ...
  private Team team;
  ...
  public void setTeam(Team team) {
    // 이미 다른 team과 연관관계 있는 member일 수 있으므로 꼭 확인하고 있으면 관계 제거해야 함
    if (this.team != null) {
      this.team.remove(this);
    }

    this.team = team;
    team.getMembers().add(this);
  }
  ...
}

  • 연관관계 편의 method 작성시 기존 연관관계를 확인하고 제거해야 함
    • owner side에만 값 넣어주는 경우와 마찬가지로 이것도 persistence context 종료 후 새 context 에서 조회 시엔 정상적으로 작동하지만 그 전까지는 연관관계 해제된 entity가 조회되기때문에 문제