내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
0. 연관관계 매핑시 고려할점 3가지
● 다중성
다대일: @ManyToOne
일대다: @OneToMany
일대일: @OneToOne
다대다: @ManyToMany => 다대다는 실무에서 사용하면 안된다.
● 단방향, 양방향
- 테이블
외래키 하나로 양쪽 Join 가능하다. 사실상 방향이라는 개념이 없다.
- 객체
참조용 필드가 있는 쪽에서 참조 대상으로만 참조가 가능하다.
한 쪽만 참조하면 단방향, 양쪽이 서로 참조하면 양방향 이다.
● 연관관계의 주인
테이블은 외래 키(FK) 하나로 두 테이블의 연관관계를 맺고 있다.
하지만, 객체의 양방향 관계는 A→B, B→A처럼 참조가 2군데에서 필요하다. => 둘중 테이블의 FK를 관리할곳이 필요하다.
연관관계의 주인: 외래 키를 관리하는 참조
주인의 반대편: 외래 키에 영향을 주지않고 단순 조회(참조)만 가능하다.
1. 다대일 [N:1]
● 다대일 단방향
가장 많이 사용하는 연관관계 이며, 다대일 의 반대는 일대다 이다.
● 다대일 양방향
왜래 키가 있는 쪽이 연관관계의 주인이다. 양쪽에서 서로 참조하도록 개발하면 된다.
연관관계가 주인이 아닌 쪽은 단순 조회만 가능하기에 필드만 추가해주면 된다.
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
2. 일대다 [1:N]
● 일대다 단방향
1이 연관관계의 주인이 되는 방식으로, 권장하지 않는 방식이다... 실무에서도 거의 사용되지 않는다.
테이블 일대다 관계는 항상 다(N) 쪽에 외래 키가 있다. => FK가 N쪽에 있는것은 생각해보면 당연하다.
Team의 members를 수정해 주면 TEAM_ID 라는 다른 테이블에 있는 FK를 update 해줘야 한다.
일대다 단방향은 1 : N 에서 1의 입장에서 연관관계를 관리하겠다는 의미이기 때문에 어찌되든 다른테이블에 있는 FK를 관리해야 한다.
객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조를 나타낸다.
Team 엔티티를 살펴보자. Team의 members 필드에 JoinColumn(name = "TEAM_ID") 로 지정되어 있다.
Team이 FK를 관리하게 되는것 이다.
@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 member = new Member();
member.setName("mamber1");
entityManager.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member); // 문제가 되는 부분
entityManager.persist(team);
transaction.commit();
실행 결과는 다음과 같다.
연관관계 관리를 위해 2개의 insert쿼리가 나간 후, 추가로 UPDATE SQL 실행하는 모습을 볼수있다.
이는 일대다(1:N)에서 저장(save)이 될 때 양 쪽 객체를 저장한 뒤 update query를 통해 외래키 설정(3번이나 수행)하기 때문!
하지만 이는 개발자 입장에서 햇갈릴수가 있다.
Team 객체를 업데이트 했는데, Member에 대한 update 쿼리문이 나가고 있기 때문이다...
- 권장하지 않는 이유
1) @JoinColumn을 꼭 사용해야 한다. 만약 그렇지 않으면 중간에 조인 테이블이 따로 생성되 버린다.
2) DB Table에서는 항상 N쪽에 FK가 있기때문에 패러다임에 충돌이 있다.
=> Entity가 관리하는 FK가 다른 테이블에 있게된다. 위에서 말한 update 쿼리가 발생한다.
3) 실무 현장에서는 테이블이 1, 2개가 아닌 수십가지가 공존한다. => 관리가 어려워 진다.
결론 : 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자
● 일대다 양방향
만약 Member 입장에서 Team에 대한 정보를 읽어오고 싶다면 어떻게 해야할까? 역방향이 하나 필요하다!
위와같은 매핑방식은 공식 스펙상으로 존재하지 않는다. 하지만 약간의 트릭을 사용하면 가능은 하다.
@JoinColumn(insertable=false, updatable=false)
읽기전용 필드로 만들어 버리는것 이다.
코드를 통해 확인해 보자.
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String name;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
// 생략...
}
위와 같이 @JoinColumn(name = "TEAM_ID")로 두면 역으로 연관관계를 만든다.
하지만 위처럼 코드를 해두면 Member의 Team이 연관관계의 주인처럼 되버린다.
(원래는 Team이 연관관계의 주인이다)
따라서 코드를 다음과 같이 바꿔 읽기 전용으로 바꾸는것 이다.
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable=false, updatable=false)
private Team team;
매핑도 되어있고, 값도 다 사용하지만, 최종적인 Update 쿼리가 만들어지지 않는다. 읽기전용 field 가 되었기 때문이다.
읽기전용 필드를 사용해서 양방향 처럼 사용하는 방법이다.
결과적으로 team에 있는 members가 연관관계 주인 행세를 한다.
결론 : 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자
3. 일대일 [1:1]
● 일대일 단방향
- 일대일 관계는 반대도 일대일 관계이다.
- 주 테이블이나 대상 테이블 중에 외래 키 선택 가능하다.
- 외래키 데이터베이스에 unique 제약 조건 추가
- 다대일 연관관계와 동일하게 외래키가 있는곳이 연관관계의 주인, 연관관계의 주인이 아닌 곳에 mappedBy를 넣어준다.
● 일대일 양방향
다대일 양방향 매핑과 같은 방식으로 외래키가 있는곳이 연관관계의 주인이다.
반대편에는 mappedBy를 적용해야 한다.
● 일대일 : 대상 테이블에 외래 키가 있는 단방향 (불가능)
위와 같은 모델은 불가능한 모델이다.
대신 대상 테이블에 외래키(FK)가 있도록 하고싶다면, 다음과 같이 양방향으로 만들어야 한다.
● 일대일 : 대상 테이블에 외래 키가 있는 양방향
근데 사실 이방법은 맨 처음 일대일방식을 대칭으로 뒤집은것 일 뿐이다.
● 정리
1. 주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾음
- 객체지향 개발자 선호
- JPA 매핑 편리
- 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능
- 단점: 값이 없으면 외래 키에 null 허용
2. 대상 테이블의 외래 키
- 대상 테이블에 외래 키가 존재
- 전통적인 데이터베이스 개발자 선호
- 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조 유지
=> unique 제약 조건만 없에도록 update 해주면 끝난다.
- 단점: 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨(프록시는 뒤에서 설명)
=> PK가 주 객체에게 있는경우,
프록시 Locker 객체를 만들려면 JPA는 Member를 로딩할때 Member에 Locker 객체 값이 있는지 없는지 알아야 한다.
Member table을 확인해서 FK가 있으면 값이 있다고 가정하고 Proxy를 생성하고, 없다면 null을 삽입해 준다.
=> PK가 대상 테이블에 있는경우,
대상 테이블인 Locker Table을 뒤져서 값이 있는지를 확인해야 한다. 이때 값이 있어야 Member의 Locker가 값이 있다고 인정된다.
어짜피 Locker를 찾는 쿼리가 나가기 때문에 Locker를 프록시로 만드는 의미가 없어진다.
4. 다대다 [N:M]
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수가 없다.
따라서 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.
하지만 객체입장은 다르다.
객체는 Member에 products 라는 참조변수를, Product에 members 라는 참조변수를 만들면 다대다 가 가능하다.
다대다가 가능한 객체와 다대다가 불가능한 테이블을 매핑하기 위해 @ManyToMany 와 @JoinTable로 연결 테이블 지정해야 한다.
우선 변경된 Member 엔티티를 확인해보자!
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
// 생략...
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
}
잘 보면 @joinColumn이 아니라, @joinTable 이다. name속성으로 중간 테이블 이름을 정해주고 있다.
실행시 생성된 테이블은 다음과 같다.
양방향으로 만들어 주려면, Product 쪽에 mappedBy를 추가해주면 된다.
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
● 다대다 매핑의 한계
- 편리해 보이지만 실무에서 사용안함.
- 연결 테이블(조인 테이블)이 단순히 연결만 하고 끝나지 않는다.
- 중간테이블에 추가적인 데이터를 넣을 수 없다는 한계점 존재.
=> 주문시간, 수량 같은 데이터가 들어올 수 있는데, 이를 반영하기가 어렵다.
- 중간 테이블이 숨겨져 있기 때문에 의도치 않은 쿼리가 생성 될 수 있음.
● 다대다 한계 극복
- 연결 테이블용 엔티티를 따로 추가(연결 테이블을 엔티티로 승격)
=> 이전에는 @JoinTable 을 사용하여 연결 테이블이 생성되었지만, 이번에는 연결 테이블을 Entity로 만들어 사용한다.
- @ManyToMany => @OneToMany, @ManyToOne
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 프록시와 연관관계 관리 (0) | 2022.04.06 |
---|---|
[JPA] 고급 매핑 (0) | 2022.04.05 |
[JPA] 연관관계 매핑 기초 (0) | 2022.04.03 |
[JPA] 엔티티 매핑 (0) | 2022.04.02 |
[JPA] 영속성 관리 - 내부 동작 방식 (0) | 2022.04.02 |
댓글