연관관계 매핑시 고려사항 3가지
다중성
- 다대일: @ManyToOne
- 일대다: @OneToMay
- 일대일: @OneToOne
- 다대다: @ManyToOne
단방향, 양방향
테이블
- 외래 키 하나로 양쪽 조인 가능
객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향
연관관계 주인
- 테이블은 외래키 하나로 두 테이블이 연관관계를 맺음
- 객체 양방향 관계는 A → B, B → A 처럼 참조가 2군데 존재해야한다.
- 객체 양방향 관계는 참조가 2군데 있음. 둘 중 테이블의 외래키를 관리할 곳을 지정해야한다.
- 연관관계 주인이 외래키를 관리하는 참조이다.
- 주인의 반대편: 외래 키에 영향을 주지 않는다. 단순히 조회만 가능
※ Source Entity / Target Entity
해당 주석에서 설명하는 Source Entity와 Target Entity는 다음과 같다.
source Entity: @JoinColumn을 사용한 entity
target Entity: @JoinColumn을 사용한 entity와 연관관계를 맺은 entity
N:1
외래키 위치: Source Entity (Member Entity)
연관관계 주인: Source Entity (Member Entity)
N:1 단방향
가장 많이 사용하는 연관관계
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne
@JoinColumn(name = "TEAM_ID") //Member테이블의 외래키 TEAM_ID와 연결
private Team team;
}
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
private List<Member> members = new ArrayList<>();
}
N:1 양방향
Team 엔티티에 mappedBy 추가
@Entity
public class Team {
@Id @GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") //양방향 매핑할 경우 추가
private List<Member> members = new ArrayList<>();
}
외래 키가 있는 쪽이 연관관계의 주인 (여기는 Member에 외래키가 있으므로 Member가 연관관계 주인)
양쪽을 서로 참조하도록 개발
만약 아래 코드와 같이 양방향에서 mappedBy를 사용하지 않으면 조인 테이블이 생긴다
@Entity
public class Team extends BaseEntity{
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
private List<Member> members = new ArrayList<>();
}
@Entity
public class Member{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username; //객체는 username사용하고 db에서는 name이라고 사용
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
1:N
외래키 위치: Target Entity (Member Entity (N))
연관관계 주인: Source Entity (Team Entity)
일대다 단방향
Entity
public class Member{
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username; //객체는 username사용하고 db에서는 name이라고 사용
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "TEAM_ID")
private List<Member> members = new ArrayList<>();
}
테이블에는 Member(N) 쪽에 외래키가 있어서 Team의 List의 값을 바꿨는데 Member의 TEAM_ID의 값이 변함.
team.getMembers().add(member); 를 하면 Member 테이블에 영향을 준다.
update 쿼리가 따로 나가게 된다.
1:N 단방향 정리
- 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인
- 테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있음 (Member 쪽에 외래키 존재)
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래 키를 관리하 는 특이한 구조
- @JoinColumn을 꼭 사용해야 함. 그렇지 않으면 조인 테이블 방식을 사용함(중간에 테이블을 하나 추가함)
@Entity
public class Team {
@OneToMany
//@JoinColumn(name = "TEAM_ID") JoinColumn을 사용하지 않으면 아래 사진 처럼 조인 테이블이 생성된다.
private List<Member> members = new ArrayList<>();
...
}
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
...
}
일대다 단방향 매핑의 단점
- 엔티티가 관리하는 외래 키가 다른 테이블에 있음
- 연관관계 관리를 위해 추가로 update SQL 실행
일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자
일대다 양방향
- 이런 매핑을 공식적으로 존재하지 않음
- Team에도 @JoinColum을 사용해서 연관관계가 2개가 된다. @JoinColumn(insertable=false, updatetable=false) 로 읽기 전용 필드를 사용해서 양방향 처럼 사용하는 방법이다.
- 다대일 양방향을 사용하자
1:1
외래키 위치: Source Entity
연관관계 주인: Soruce Entity
외래 키에 데이터베이스 유니크(UNI) 제약조건 추가
일대일 단방향
다대일(@ManyToOne) 단방향 매핑과 유사
여기서는 Member 테이블을 Source Entity로 정함
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
...
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
...
}
@Entity
public class Locker {
@Id @GerneratedValue
private Long id;
...
}
일대일 양방향
멤버에 @JoinColumn으로 FK를 설정했으면 Locker에 member 필드 추가해서 양방향 매핑
@Entity
public class Locker {
@Id @GerneratedValue
private Long id;
//추가
@OneToOne(mappedBy = "locker")
private Member member;
...
}
만약 Member 객체에 있는 locker 필드가 연관관계 주인이고 외래키가 locker 테이블에 있는 경우
이 경우 연관관계 매핑이 안된다.
대상 테이블에 외래키 단방향 관계는 jpa 지원하지 않고 양방향 관계는 지원 (일대일 관계에서는 Team이 연관관계 주인이고 Member 테이블의 FK를 관리하는 형식이 지원되지 않는다)
양방향이면 Locker 엔티티 자신이 Locker 테이블 관리
일대일 관계는 자신의 엔티티에 있는 외래키는 본인만 관리할 수 있다.
Q. 그럼 일대일 관계에서는 누가 외래키를 가지고 있는 것이 더 좋은가?
locker에 외래키가 있는 경우 나중에 하나의 회원이 여러 개의 locker를 가질 수 있다면 그 경우 locker에 있는 member의 외래키에서 unique 제약조건을 제거하면 된다. (locker : member = N : 1)
하지만 member 테이블에 외래키가 있는 경우 하나의 회원이 여러개의 locker를 가지는 시나리오가 되면 변경할 게 많아진다.
member 테이블을 select 많이한다는 가정하에
개발자 입장에서는 member에 외래키가 있는 게 유리하다.
→ member를 가져오면 locker의 값도 가져오므로 쿼리 하나로 locker 값의 유무를 판단 할 수 있기 때문이다.
위 가정하에 locker에 왤키가 있으면 member의 locker를 조회하기 위해서
locker의 테이블을 가져온다 → locker 테이블에서 where로 member_id 조회 → 값이 있으면 해당 member가 locker를 가지고 있다고 확인. 지연 로딩으로 설정해도 항상 즉시 로딩 된다.
일대일 정리
주 테이블에 외래키
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
- 객체지향 개발자 선호
- JPA 매핑 편리
- 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
- 단점: 값이 없으면 외래 키에 null 허용
대상 테이블에 외래키
- 대상 테이블에 외래 키가 존재
- 전통적인 데이터베이스 개발자 선호
- 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
- 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨
N:M
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음.
연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함.
N:M 단방향
@Entity
public class Member {
@ManyToOne
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
...
}
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
}
N:M 양방향
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToMany(maappedBy = "products")
private List<Member> members = new ArrayList<>();
}
mappedBy를 넣어줘야한다.
다대다
- @ManyToMany 사용
- @JoinColumn로 연결 테이블 지정
다대다 매핑의 한계
중간에 연결 테이블에 추가 정보가 들어갈 수 없다.
다대다 한계 극복
연결 테이블용 엔티티 추가
@Entity
public class Memeber {
...
//연결용 엔티티와 매핑
@OneToMany("member")
private List<MemberProduct> memberProducts = new ArrayList<>();
...
}
@Entity
public class Product {
...
//연결용 엔티티에 product 필드와 매핑
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
....
}
'JPA' 카테고리의 다른 글
값 타입과 불변 객체와 값 타입 컬렉 (4) | 2023.11.10 |
---|---|
갑 타입 (1) | 2023.11.10 |
영속성 전이: CASCAED (0) | 2023.11.09 |
즉시 로딩과 지연 로딩 (2) | 2023.11.09 |
프록시 (1) | 2023.11.09 |