연관관계 매핑시 고려사항 3가지
- 다중성
- 다대일 : @ManyToOne
- 일대다 : @OneToMany
- 일대일 : @OneToOne
- 다대다 : @ManyToMany
- 단방향, 양방향
- 테이블(단방향) : 외래 키 하나로 양쪽 조인이 가능하다 즉 방향이라는 개념 자체가 없다.
- 객체(양방향) : 객체는 참조용 필드가 있는 쪽으로만 참조가 가능하다. 한쪽만 참조하면 단방향이고, 양쪽이 참조하면 양방향이다.
- 연관관계의 주인
- 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺는다.
- 객체 양방향 관계는 A->B, B->A처럼 참조가 2군데이다.
- 객체 양방향 관계는 참조가 2군데 있음, 둘중 테이블의 외래키를 관리할 곳을 지정해야 한다.
- 연관관계의 주인 : 외래키를 관리하는 참조
- 주인의 반대편, 외래 키에 영향을 주지 않음, 단순 조회만 가능하다.
다중성
1. 다대일(@ManyToOne)
테이블 기준으로 TEAM_ID는 FK이기 때문에 TEAM값이 들어와있다.
그래서 Member객체 기준으로는 TEAM_ID에 연관관계를 매핑해주면 된다
Member의 team이나 MEMBER의 TEAM_ID이나 둘 다 Team을 찾아가기 위해 만들어져있는 것들이기 때문이다.
다대일 단방향 그림 예시는 위와 같다. DB 입장에서 보면 TEAM은 1이고 MEMBER는 N이기 때문에 외래키는 무조건 MEMBER쪽으로 가야한다.(다 쪽엔 항상 외래키가 들어가야한다.)
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
양방향은 반대쪽에서 참조하기만 하면 된다.
@OneToMany(mappedBy = "team") //Member엔티티에 team변수명에 걸려 있다.
private List<Member> members = new ArrayList<>();
2. 일대다(@OneToMany)
여기에서는 1이 연관관계의 주인이다. 이 모델은 보통 권장되지 않는다.
Member입장에서는 Team을 모르나 Team입장에선 Member 리스트를 갖는다.
하지만 DB입장에서는 무조건 다(MEMBER)쪽에 외래키가 있어야 한다.(팀이 1이고 멤버가 다이기 때문)
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
public Member() {
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
@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<>();
public List<Member> getMembers() {
return members;
}
public void setMembers(List<Member> members) {
this.members = members;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
try {
Member member = new Member();
member.setUsername("member1");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);
tx.commit();
위의 코드를 돌려보면 아래와 같은 결과가 나온다.
select * from member;
MEMBER_ID TEAM_ID USERNAME
1 | 1 | member1 |
select * from team;
TEAM_ID NAME
1 | teamA |
Hibernate:
/* insert for
hellojpa.Member */insert
into
Member (USERNAME, MEMBER_ID)
values
(?, ?)
Hibernate:
/* insert for
hellojpa.Team */insert
into
Team (name, TEAM_ID)
values
(?, ?)
Hibernate:
update
Member
set
TEAM_ID=?
where
MEMBER_ID=?
team의 입장에서는 Member의 TEAM_ID(FK)를 수정할 수 있는 방법이 없다. 따라서 어쩔 수 없이 update쿼리를 한번 더 쳐야한다 -- 성능 상 단점이 존재한다.
그리고 실무에서는 테이블이 수십개가 엮여서 돌아가는 상황인데, @OneToMany를 사용하게 되면 운영이 복잡해진다.
.
일대다 단방향 정리
- 일대다 단방향은 일대다(1:N)에서 일(1)이 연관관계의 주인이다.
- 테이블 일대다 관계는 항상 다(N)쪽에 외래 키가 있다.
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조이다.
- @JoinColumn을 꼭 사용해야 한다. 그렇지 않으면 조인 테이블 방식을 사용한다.(중간에 테이블을 하나 추가한다.)
- 일대다 단방향 매핑의 단점
- 엔티티가 관리하는 외래 키가 다른 테이블에 있다.
- 연관관계 관리를 위해 추가로 UPDATE SQL을 실행한다.
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자.
3. 일대일(@OneToOne)
- 일대일 관계는 그 반대도 일대일이다.
- 주 테이블이나 대상 테이블 중 외래키를 선택할 수 있다.
- 주 테이블에 외래 키
- 대상 테이블에 외래 키
- 외래 키에 데이터베이스 유니크(UNI) 제약조건 추가
** 유니크 값 : 테이블 열에 중복된 값을 허용하지 않는다. **
회원이 주 테이블이라고 가정한 그림이다. 멤버는 락커를 한개만 가져야 하고, 마찬가지로 락커도 멤버를 한개만 가져야 한다. 위를 토대로 코드를 작성해보면 다음과 같다.
@Entity
public class Locker {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "locker")
private Member member;
}
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
다대일 양방향 매핑처럼 외래 키가 있는 곳이 연관관계의 주인이다.
다대일 양방향과 마찬가지로 반대편은 mappedBy를 적용한다.
참고 : 대상 테이블에 외래 키 단방향 관계는 jpa에서 지원하지 않는다.
주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것 처럼, 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
- 객체지향 개발자들이 선호한다
- jpa 매핑이 편리하다
- 장점 : 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하다.
- 단점 : 값이 없으면 외래 키에 null을 허용해야한다.
대상 테이블에 외래 키
- 대상 테이블에 외래키가 존재하는 경우이다.
- 전통적인 데이터베이스 개발자가 선호한다.
- 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 유지한다.
- 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩된다.
4. 다대다(@ManyToMany)
다대다는 실무에서 쓰면 안된다. 그렇기에 가볍게 보도록 하자.
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 었다.
- 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.
다대다는 @ManyToMany를 사용하고, @JoinTable로 연결 테이블을 지정할 수 있다.
객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
@Entity
public class Product {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@Entity
public class Member {
...
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
다대다 매핑의 한계
- 편리해 보이지만 실무에서 사용할 수 없다.
- 연결 테이블이 단순히 연결만 하고 끝나지 않는다.
- 주문시간, 수량 같은 데이터가 들어올 수 있다.
이런 다대다 매핑의 한계를 극복하기 위해 연결 테이블용 엔티티를 추가한다.(연결 테이블을 엔티티로 승격시킨다)
- @ManyToMany -> @OneToMany, @ManyToOne
참고 : PK는 의미없는 값인 USER_ID를 쓰거나 @GeneratedValue를 쓰거나 해서 PK를 한개만 잡고 가면 JPA 맵핑도 심플해지고, 개발할때 유연하고 쉬워진다. 필요할때마다 제약조건을 추가하자.
실전 예제 - 다양한 연관관계 매핑
- 주문과 배송은 1:1(@OneToOne)
- 상품과 카테고리는 N:M(@ManyToMany)
위를 기반으로 코드를 작성해보자.
'공부 > JPA' 카테고리의 다른 글
프록시와 연관관계 정리 (0) | 2024.12.21 |
---|---|
JPA 고급 매핑 (2) | 2024.12.08 |
연관관계 매핑 기초 (0) | 2024.12.02 |
엔티티 매핑 (0) | 2024.11.30 |
JPA 영속성 관리 (1) | 2024.11.28 |