-
6. [JPA] 다양한 연관 관계매핑 방법웹개발/Hibernate(JPA) 2020. 4. 10. 15:52
안녕하세요 현우입니다. 이번 포스팅은
[ 다양한 연관관계 매핑 방법 그리고 상황에 따른 매핑 사용방법 TiP ] 입니다.
참고서 http://acornpub.co.kr/book/jpa-programmig
자바 ORM 표준 JPA 프로그래밍
JPA 기초 이론과 핵심 원리, 그리고 실무에 필요한 성능 최적화 방법까지 JPA에 대한 모든 것
www.acornpub.co.kr
연관관계를 매핑시에 다음 3가지를 고려해야 합니다.
1. 다중성 (모두 데이터베이스의 연관관계를 생각한다)
- 다대일(@ManyToOne)
- 일대다(@OneToMany)
- 일대일(@OneToOne)
- 다대다(@ManyToOne)
보통 다대일(@ManyToOne)과 일대다(@OneManyToOne) 관계를 가장 많이 사용하고 다대다 관계는 실무에서 거의 사용하지 않습니다.
2. 단방향, 양방향
- 테이블
- 테이블은 외래키 하나로 양쪽 조인이 가능하여 사실상 방향이라는 개념이 없습니다.
- 객체
- 참조용 필드가 있는 쪽으로만 참조가 가능합니다.
- 한쪽만 참조하면 단방향입니다.
- 양쪽이 서로 참조하면 양방향 입니다. ( 양방향이란 사실성 존재하지않는다. 사실은 단방향이 두개라는 의미)
3. 연관관계 주인
테이블은 외래키 하나로 두 테이블이 연관관계를 맺지만 객체 양방향 관계는 A->B, B->A 처럼 참조가 2군데이다.
객체 양방향 관계는 참조가 2군데있고 둘중에 외래키를 관리할 곳을 지정해야 한다(OWNER)
- 연관관계 주인 : 외래키를 관리하는 참조 OWNER
- 주인의 반대편 : 외래키에 영향을 주지않음, 단순조회만 가능하다.
1. 다대일 N:1
(1) 다대일 단방향 매핑
다대일 관계의 방향은 항상 일대다 관계고 일대다 관계의 반대 방향은 항상 다대일 관계입니다.
데이터베이스 테이블의 일(1), 다(N) 관계에서 외래 키는 항상 다쪽에 있습니다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽입니다.N:1 두 회원을 같은 팀에 넣는다고 가정한다 @Entity class Member{ @ManyToOne @JoinColumn(name="team_id") private Team team; } @Entity class Team { // 단방향 }
(2) 다대일 양방향 매핑 (테이블에 영향은 주지 않는다)
-
양방향은 외래 키가 있는 쪽이 연관관계의 주인입니다.
일대다와 다대일 연관관계는 항상 다(N)에 외래 키가 있습니다. 여기서는 다쪽인 MEMBER 테이블이 외래 키를 가지고 있으므로 Member.team이 연관관계의 주인입니다. JPA는 외래 키를 관리할 때 연관관계의 주인만 사용합니다. 주인이 아닌 TEAM.members는 조회를 위한 JPQL이나 객체 그래프를 탐색할 때 사용합니다. -
양방향 연관관계는 항상 서로를 참조해야 합니다.
양방향 연관관계는 항상 서로 참조해야 합니다. 어느 한 쪽만 참조하면 양방향 연관관계가 성립하지 않습니다. 항상 서로 참조하게 하려면 연관관계 편의 메서드를 만드는 것이 좋습니다.
@Entity class Member{ @ManyToOne @JoinColumn(name="team_id") private Team team; } @Entity class Team { @OneToMany(mappedby="team") private List<Member> members = new ArrayList<>(); }
2. 일대다 1:N
일대다 관계는 다대일 관계의 반대 방향입니다. 일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션인Collection, List, Set, Map 중에 하나를 사용해야 합니다.
(1) 일대다 단방향 매핑
하나의 팀은 여러 회원을 참조할 수 있는데 이런 관계를 일대다 관계라 합니다. 그리고 팀은 회원들을 참조하지만 반대로 회원은 팀을 참조하지 않으면 둘의 관계는 단방향입니다.
One에 속하는 Team을 중심으로 주인을 둡니다. Team에 외래키를 두면 계속해서 Team을 Insert해줘야 하므로 DB에서는 무조건 Member에 외래키를 둬야합니다.
1:n 팀을 주인으로 한다. 하지만 JOIN되는 컬럼의 외래키는 member에 둔다. @OneToMany @JoinColumn(name = "team_id") //Member테이블의 TEAM_ID (FK) private List<Member> members = new ArrayList<>(); ------------------------------------------------------------ Member member = new member(); member.setUsername("member1"); em.persist(member); Team team = new Team(); team.setName("TeamA"); team.getMembers().add(member); em.persist(team);
위의 코드를 실행한 결과입니다. 왜 UPDATE SQL이 호출 되었을까요?
UPDATE SQL 그 이유는 외래키를 가지고 있지 않은 TEAM이 MEMBER을 관리하기 때문입니다. MEMBER 엔티티를 저장할 때는 TEAM의 정보를 모르기 때문에 MEMBER 테이블의 TEAM_ID 외래키에 아무값도 저장되지 않습니다. 대신 TEAM 엔티티를 저장할 때 Team.members의 참조값을 확인해서 회원테이블에 있는 TEAM_ID외 래키를 업데이트 하는 것 입니다.
- Insert into Member (Member_ID, username) values (?, ?);
- Insert into Team (name, TEAM_ID) values (?, ?);
- Update Member set Team_ID=? where Member_ID=? *update쿼리문이 한번 더 나간다.
정리
일대다(1:N) 일대다 단방향은 일(1)이 연관관계의 주인이다. 테이블 일대다 관계는 항상 다(N)쪽에 외래키가 있다. 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조이다. 결론 . 일대다 단방향 매핑보다 다대일 양방향 매핑을 사용하자
일대다 는 엔티티를 매핑한 테이블이 아닌 다른 테이블의 외래키를 관리 해야한다. 이것은 성능 문제도 있지만 실무에서 수십개의 테이블이 엮여서 돌아가는 상황에서의 관리문제도 생긴다. 다대일 양방향 매핑은 관리해야 하는 외래키가 본인 테이블에 있기 때문에 일대다와 같은 이슈는 생기지 않는다.
일대다 양방향매핑
일대다 양방향 매핑은 공식적으로 존재하지는 않습니다.
관계형 데이터베이스의 특정상 @OneToMany는 연관관계의 주인이 될 수 없습니다. @OneToMany, @ManyToOne 둘 중에 연관관계의 주인은 항상 다(N) 쪽인 @ManyToOne을 사용한 것 입니다. 이런 이유로 @ManyToOne에는 mappedBy속성이 없지만 양방향 매핑이 완전히 불가능 한 것은 아닙니다. 일대다 단방향 매핑 반대편에 같은 외래키를 사용하는 다대일 단방향 매핑을 읽기 전용으로 추가하면 됩니다.
일대다 양방향 class Team{ @OneToMany @JoinColumn(name="Team_ID") private List<Member> members = new ArrayList<>(); } class Member{ @ManyToOne @JoinColumn(name ="Team_ID", Insertable = false, updatable=false) //오로지 읽기전용 private Team team; }
Member.team 속성에 Insertable = false, updatable=false을 추가하여 읽기전용으로 만들었습니다. (Insert SQL문 x)
정리
이러한 매핑은 공식적으로 존재하지 않는다
읽기 전용 필드를 사용해서 양방향 처럼 사용하는 것 뿐이다. @JoinColumn(insertable=false, updatable=false)
테이블이 수십개씩 돌아갈때 매핑은 최대한 단순하게 설계해야하므로 왠만하면 ManyToOne 양방향을 추천한다.
3. 일대일 1:1
일대일 관계는 양쪽이 서로 하나의 관계만 가진다. 일대일 관계는 그반대도 일대일 이며 주 테이블이나 대상 테이블 중에 외래키 선택이 가능하다. 주 테이블에 외래키를 넣어도 되고 대상 테이블에 외래키를 넣어도 된다.
또 외래키에 데이터베이스 유니크 제약조건을 추가하여 관리 하기도 한다.
일대일: 주 테이블에 외래키 단 방향
일대일 관계 class Locker{ ... @OneToOne(mappedby="member") //양방향일때만 private Member member; } class member{ ... @OneToOne @JoinColumn(name="locker_id"); private Locker locker; }
다대일 단방향 매핑과 유사하다.
일대일: 대상 테이블에 외래키 단 방향
대상 테이블의 단방향은 지원이안된다.
양방향은 지원한다.
일대일 관계에서는 각자의 키값만 관리해주면된다.
DBA관점에서 생각해보자. 일대일 매핑시에 Member 와 Locker중에 어떤 테이블에서 외래키를 관리하는 것이 좋을까?
추후 서비스가 변동되어 Member 당 여러개의 Locker을 관리 할 수 있게 되었을때, Locker에 외래키를 두는 경우 가 테이블을 수정하기 쉬울 것이고 Locker 당 여러 Member의 경우 당연히 Member에서 외래키를 관리하는것이 쉬울 것 이다. 하지만 현재 개발중인 개발자의 입장에서는 Member에 Locker_id가 있는 것이 편리하다.
개발시 Member을 select를 많이 하게되며 Locker가 없을 경우 null값을 주면 된다. 대부분의 비니지스에 Member은 조회를 해와야 한다. 즉 DB쿼리 한번으로 Locker에 대한 값을 판단할 수 있다. 경우에 따라서 선택하자.
정리
주 테이블 외래 키
- 주 객체가 대상객체의 참조를 가지는 것 처럼 주 테이블에 외래키를 두고 대상 테이블을 찾을 수 있다.
- 주 태이블만 조회해도 대상테이블에 데이터가 있는지 확인이 가능 하다.
- 값이 없으면 외래키에 Null을 허용해야 한다.(*치명적...)
대상 테이블 외래 키
- 대상 테이블에 외래키를 만들 수 있다.
- 데이터 베이스 개발자들이 선호하는 방식이다. (null 문제 해결)
- 주 테이블과 대상 테이블을 일대다에서 일대일 관계로 변경할 때 테이블 구조 유지
- 프록시 기능의 한계로 지연로딩으로 설정해도 항상 즉시 로딩된다.
4. 다대다 N:M
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야 한다.
객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
Member은 Product 리스트를 가질 수 있고 Product도 Memer의 리스트를 가질 수 있다.
- @ManyToMany 사용
- @JoinTable로 연결 테이블 지정이 가능
- 다대다 매핑: 단방향, 양방향
Member 와 Product 다대다 관계 코드
1. product 클래스
@Entity @Getter @Setter public class Product{ @Id @GeneratedValue private Long id; private String name; @ManyToMany(mappedby = "products") //양방향적용 private List<Member> members = new ArrayList<>(); }
2. Member 클래스
@Entity @Setter @Getter public class Member{ @Id @GenerateValue private Long id; private String username; @ManyToOne private Team team; //다대다 매핑 @ManyToMany @JoinTable(name = "MEMBER_PRODUCT") private List<Product> products = new ArrayList<>(); }
3. 결과
MEMBER_PRODUCT 생성 외래키 제약조건 2가지(PK,FK) 다대다 매핑 한계
실제로 다대다 매핑은 겉으로 보기엔 편리해 보이지만 절대로 실무에서 사용하면 안된다. 연결 테이블이 단순히 연결만하고 끝나지 않는다. 서비스를 진행하면서 주문시간 , 수량 , 변경시간 등 추후에 여라 가지 데이터 컬럼이 무조건 추가될 것이다. 하지만 중간테이블 Member_Product에서 매핑정보만 데이터컬럼으로 가능하고 추가 정보 삽입이 불가능 하다.
매핑 테이블의 한계 그 외에 실행 후 콘솔창을 보면 쿼리문이 이상하게 나가는 것을 확인 할 수 있다. Member와 Product를 Join할때 중간테이블이 숨겨져 있기 때문에 내가 생각하지 못하는 쿼리문이 나간다.
다대다 한계 극복
중간 매핑테이블을 연결 테이블용 엔티티로 추가한다(연결테이블 엔티티승격) 그리고 @ManyToMany 대신 @OneToMany, @ManyToMany를 사용하도록 하자
1. Member 클래스
@Entity @Setter @Getter public class Member{ @Id @GenerateValue private Long id; private String username; @ManyToOne private Team team; //다대다 매핑 @OneToMany(mappedby="member") private List<MEMBER_PRODUCT> member_products; }
2. Product 클래스
@Entity @Getter @Setter public class Product{ @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedby = "products") //양방향적용 private List<MEMBER_PRODUCTS> member_products = new ArrayList<>(); }
3. MEMBER_RPODUCTS 클래스
@Entity @Getter @Setter public class MEMBER_PRODUCT{ @Id @GeneratedValue private Long id; @ManyToOne @JoinColumn(name = "MEMBER_ID") private Member member; @ManyToOne @JoinColumn(name = "PRODUCT_ID") private Product product private int count; private int price; ... }
실제 비지니스는 매우 복잡하여 ManyToMany로는 로직을 풀수가 없다.
코드 자세히 보기
https://github.com/HyeonWuJeon/KimYoungHan-JPA
HyeonWuJeon/KimYoungHan-JPA
자바 ORM표준 JPA 프로그래밍 책 정리 및 예제 작성. Contribute to HyeonWuJeon/KimYoungHan-JPA development by creating an account on GitHub.
github.com
'웹개발 > Hibernate(JPA)' 카테고리의 다른 글
10. 즉시 로딩과 지연 로딩 (0) 2020.04.28 9. 프록시와 연관관계 (0) 2020.04.25 7. 고급매핑 (0) 2020.04.20 5. [JPA] 양방향 연관관계와 연관관계 주인 (0) 2020.04.01 4. [JPA] 연관관계 매핑 (0) 2020.04.01