이후 참조하고 있는 Team의 외래 키(FK)를 지정하는 @JoinColumn를 통해 Member와 TEAM_ID 외래키를 매핑해 준다.
(해당 어노테이션을 생략하면 기본적으로 "필드명_기본키" 형식으로 외래 키가 자동 생성된다.)
--
양방향 연관관계
--
위 그림은 N:1 양방향 연관관계다. (N : Member, 1 : Team)
양방향 연관관계는
두 엔티티가 서로를 참조하는 방식으로
두 객체가 서로 관계에 따라 탐색(접근)이 가능하다.
즉, Member에서는 Team을 참조하고 있기 때문에 Member에서 Team을 탐색할 수 있고
반대로 Team에서는 Member를 List 형식으로 참조하고 있기 때문에 Member에 대해 탐색이 가능하다.
위 그림에서
객체 연관관계를 보면 Team 객체에 members라는 List가 추가되어 있다.
(하나의 Team에서 여러 개의 Member를 가질 수 있기 때문이다.)
다만 테이블 연관관계를 보면 단방향 연관관계와 차이가 없어 보인다.
사실 테이블 연관관계에서는 방향이라는 개념이 없고, 그냥 외래키 하나 넣으면 양방향으로 접근이 가능하다.
즉, Member 입장에서는 team_id 외래키로 Team의 team_id를 조인하면 Team에 접근할 수 있으며,
반대로 Team에서는 team_id와 Member의 team_id 외래키를 조인하면 접근할 수 있다.
하지만 객체 연관관계에서는 불가능하기 때문에 반대 측 객체에 List 형식으로 추가된 것이다.
양방향 연관관계 예시 코드
@Entity
public class Member {
...
@ManyToOne
@JoinColumn(name = "team_id")
private Team team;
...
}
@Entity
public class Team {
...
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<Member>();
...
}
N:1 연관관계이기 때문에
Member 객체에서는 @ManyToOne을, Team 객체에서는 @OneToMany를 선언한다.
@OneToMany를 사용할 때는 "mappedBy" 옵션을 작성해야 한다.
mappedBy 옵션은 현재 해당 @OneToMany가 무엇과 연결되어 있는지 설정하는 것으로 위 코드를 예시로 들면 members라는 List는 Member 객체의 team이라는 필드와 연결되어 있다고 설정하는 것이다. (Member 객체의 team에 의해서 관리된다.)
즉, 해당 필드는 무엇으로 인해 매핑이 되어있다는 의미다.
이미 반대쪽에서 연결을 했는데 굳이 "mappedBy" 옵션을 또 지정해야 하는 걸까?
"mappedBy" 옵션 사용 이유를 이해하기 위해서는 객체와 테이블 간에 연관관계에 대해 이해해야 한다.
객체 간에 연관관계에는 2가지 존재 1. 회원 => 팀 (단방향) 2. 팀 => 회원 (단방향) [ 사실 객체의 양방향 연관관계는 서로 다른 단방향 관계 두 개를 사용하여 양방향처럼 사용하는 것이다. ]
테이블 간에 연관관계에는 1가지 존재 1. 회원 <=> 팀 (양방향) [ 외래 키 하나만 있으면 서로 조인을 통해 양방향 가능]
이때 Member의 team과 Team의 members 중에서 무엇을 Member 테이블의 team_id 외래키와 매핑을 해야 할까?
즉, Member의 team 값이 바뀔 때 Member 테이블의 team_id 외래키가 업데이트되어야 하나? Team의 members 값이 바뀔 때 Member 테이블의 team_id 외래키가 업데이트 되어야 하나?
참고로 외래키를 가진 테이블(Member) 입장에서는 객체가 무엇을 하든 자신의 외래키 값만 업데이트되면 된다.
예시로 Member의 team에는 값을 넣고 Team의 members에는 값을 넣지 않거나 반대로 Team의 members에는 값을 넣었지만 Member의 team에는 값을 넣지 않은 경우가 존재한다. 그러면 무엇을 외래키와 매핑을 해줘야 할까?
이러한 상황으로 인해 규칙(룰)이 생긴다. - 둘 중 하나로 외래 키를 관리해야 한다. 즉, 관리할 주인을 정해야 한다.
이를 "연관관계의 주인"이라고 부른다. 즉, 양방향 연관관계에서 주인을 정의하기 위해 "mappedBy" 옵션을 사용하는 것이다.
양방향 매핑 규칙
객체 관계 중 하나를 연관관계의 주인으로 지정 (외래키가 존재하는 곳을 주인으로 정하는 것을 권장)
연관관계의 주인만이 외래 키를 관리 (등록, 수정 등)
주인이 아닌 쪽은 읽기만 가능
주인은 "mappedBy" 옵션 사용 금지
주인이 아니면 "mappedBy" 옵션으로 주인 지정
위 코드에 대한 설명
주인은 Member의 team
주인인 Member의 team은 외래키를 관리 가능
Team의 members는 읽기만 가능 (members에 값을 넣어 봤자 아무런 변화 없음)
주인인 Member의 team에는 "mappedBy" 옵션 사용 금지
주인이 아닌 Team의 members에 "mappedBy" 옵션을 사용하여 주인 지정
Member의 team에 값을 넣고 (Team의 members에는 값을 넣지 않음) Team의 members를 조회하면 Member의 team에 넣은 값을 조회할 수 있다.
다만 문제가 생긴다.
문제 1. Member의 team에 값을 넣을 때 영속성 컨텍스트에 들어가 있는 상태로 아직 DB에 적용되지 않아 Team의 members에 값이 존재하지 않아 조회가 불가능하다. 그래서 주인 쪽(Member)에만 값을 넣어주는 것보다 양쪽 모두 값을 넣어주는 것이 좋다.
물론 Member의 team에 값을 넣고 바로 영속성 컨텍스트에 "flush()", "clear()"로 영속성 컨텍스트에 담아둔 내용들을 DB에 적용하고 청소하면 Team에서 조회가 가능해진다.
문제 2. 테스트케이스를 작성할 때는 JPA 없이도 동작하도록 작성하는데 (순수 자바 코드로 작성) 이때 Team의 members를 조회할 때 null로 조회가 불가능하다. 그래서 이 또한 양쪽 모두 값을 세팅해 주는 것이 좋다.
결과적으로 양쪽에 모두 값을 넣어줘야 하다 보니 가끔 코드를 작성할 때 까먹을 때가 존재하여 편의를 위해 한쪽에 값을 넣으면 다른 쪽에도 값이 넣어지게 메서드를 작성하기도 한다.
public void changeTeam(Team team) { this.team = team; team.getMembers().add(this); }
참고 양방향보다는 단방향 매핑으로 설계하는 것이 좋다. 양방향 매핑은 반대 측에서 조회 기능만 추가된 것으로 객체 입장에서는 굳이 양방향에 대해 좋은 것이 없고 이에 대한 대처 고민거리만 많아진다.
--
1:1, 1:N, N:1, N:N 연관관계
--
연관관계 매핑 시 고려사항
다중성 [1:1, 1:N, N:1, N:N]
단방향, 양방향
연관관계 주인
1:1 연관관계 (One-to-One)
하나의 엔티티가 다른 하나의 엔티티와 1:1로 매핑되는 관계다.
1대1 관계이기 때문에 외래키를 원하는 곳에 선택하여 정의할 수 있다.
다만 DB 입장에서는 외래키에 유니크 제약 조건이 추가로 필요하다.
1:1 양방향 (주 테이블에 외래키) 1:1 단방향 (대상 테이블에 외래키)
이러한 관계는 JPA에서 지원하지 않는다. 다만 양방향에서는 지원한다. (그냥 주인을 반대쪽으로 옮겨서 매핑하게 된다.)
1:1 양방향 (대상 테이블에 외래키)
1:N 연관관계 (One-to-Many)
하나의 엔티티가 여러 개의 엔티티와 관계를 맺는 관계다.
이러한 연관관계는 권장하지 않는다.
이러한 설계는 Team에서 Member의 내용을 알고 싶지만,
Member에서는 Team의 내용이 필요 없는 경우의 설계다.
특징
1(Team)이 연관관계의 주인
다만 테이블에서는 항상 N(Member)쪽에 외래키가 존재하여 연관관계의 주인이 반대편 테이블의 외래키를 관리하게 되는 구조가 된다.
그리고 꼭 @JoinColumn을 사용해야 한다. (그러지 않으면 조인 테이블을 사용하게 되어 중간에 테이블이 새로 하나 추가된다.)
단점
엔티티가 관리하는 외래키가 다른 테이블에 존재
연관관계 관리를 위해 추가로 UPDATE SQL문이 실행
1:N 양방향은 공식적으로 존재하는 것은 아니지만 억지로 만들 수는 있다.
N:1 연관관계 (Many-to-One)
여러 개의 엔티티가 하나의 엔티티와 연결되는 관계다.
단방향양방향
N:N 연관관계 (Many-to-Many)
여러 개의 엔티티가 서로 여러 개의 엔티티와 연결되는 관계다.
관계형 DB에서는 정규화된 테이블 2개로 N:N 관계를 표현할 수 없다.
그래서 정규화를 위해 테이블 사이에 중간 테이블을 추가하여 각각 1:N, N:1 관계로 풀어내야 한다.
하지만 객체 관계에서는 컬렉션을 사용하여 객체 2개로 N:N 관계가 가능하다.
다만 ORM 입장에서는
객체는 가능하지만 테이블로는 불가능하기 때문에 무언가를 지원해야 한다.
그래서 위의 그림처럼 중간에 테이블을 만들어서 N:N 연관관계인 것처럼 구현이 가능하게 만들어서 매핑해 준다.
예시 코드
@Entity
public class Student {
@Id @GeneratedValue
private Long id;
@ManyToMany
@JoinTable(
name = "student_course",
joinColumns = @JoinColumn(name = "student_id"),
inverseJoinColumns = @JoinColumn(name = "course_id")
)
private List<Course> courses = new ArrayList<>();
}
@Entity
public class Course {
@Id @GeneratedValue
private Long id;
}
@JoinTable을 통해 연결할 테이블을 지정해 줄 수 있다.
(Student_Course처럼 중간에 새로 생길 테이블 이름)
편리해 보이지만 N:N 매핑의 한계로 인해
사실 실무에서는 사용이 불가능하다.
연결 테이블이 단순히 두 테이블을 연결하고 끝나는 것이 아니라
그 안에 추가로 다른 데이터가 들어올 수 있게 해야 하는데
여기서는 매핑 정보만 넣어지며 그 외의 추가적인 것은 넣을 수 없다.
즉, Student_Course 테이블에는 student_id와 course_id만 들어가고 다른 속성(수업 시간, 수업 책 등)은 넣을 수 없다.