JPA) 연관관계 매핑 - 다양한 연관관계
Date: Updated:카테고리: java
연관관계 매핑시 고려사항
@JoinColumn
-
속성 설명 기본값 name 매핑할 외래 키 이름 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명 referencedColumnName 외래 키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본키 컬럼명 foreignKey(DDL) 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다. unique, nullable, insertable, updatable, columnDefinition, table @Column의 속성과 같다.
-
- 다중성
- 1:N(
@OneToMany
) - N:1(
@ManyToOne
) - 1:1(
@OneToOne
) - N:M(
@ManyToMany
)- 권장하지 않음.
- 1:N(
- 단방향, 양방향
- 테이블
- 외래키 하나로 양쪽 조인이 가능하다.
- 방향이라는 개념 X
- 객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향
- 양쪽이 서로 참조하면 양방향
- 테이블
- 연관관계의 주인
- 테이블은 외래키 하나로 두 테이블이 연관관계를 맺음.
- 객체 양방향 관계는 A -> B, B -> A 처럼 참조가 2군데임.
- 객체 양방향 관계는 참조가 2군데있음. 둘중 테이블의 외래키를 관리할 곳을 지정해야함.
- 연관관계의 주인 : 외래키를 관리하는 참조
- 주인의 반대편 : 외래키에 영향을 주지않고 단순 조회만 (read-only)
다대일 (N:1)
다대일의 관계에서 다(N)를 연관관계의 주인으로 설정한다.
@ManyToOne
-
속성 설명 기본값 optional false로 설정하면 연관된 엔티티가 항상 있어야 한다. TRUE fetch 글로벌 페치 전략을 설정한다. @ManyToOne
=FetchType.EAGER,@OneToMany
=FetchType.LAZYcascade 영속성 전이 기능을 사용한다. targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.
-
- 다대일 단방향
- 설계 의도
Member
가 속한Team
을 추가하거나 수정하고 싶을 때
- DB 입장에선 Member와 Team의 관계가 N:1이므로 외래키가 Member쪽에 속한다.
- N쪽에 항상 외래키가 존재해야함.
- 객체 입장에선 외래키가 존재하는 엔티티에 참조용 필드를 만들어서 연관관계를 설정하면 된다.
@Entity public class Member { ... @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; }
- 설계 의도
- 다대일 양방향
- 설계 의도
Member
입장에서Member
가 속한Team
도 추가하거나 수정할 수 있고Team
입장에서Team
에 속한Member
들을 조회해야 할 때
- 연관관계 주인으로 설정된 반대편에 참조용 객체를 생성하여 이어주면 된다.
@Entity public class Team { ... @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); }
- 반대쪽에 참조용 객체를 추가한다고 해도 테이블에 영향을 주지않는다.
- 어차피 read-only 이므로
- 외래키가 있는 쪽이 연관관계의 주인이다.
- 양쪽을 서로 참조하도록 개발한다.
- 설계 의도
가장 많이 사용하는 연관관계이며 다대일의 반대는 일대다 이다.
일대다 (1:N)
일대다의 관계에서 일(1)을 연관관계 주인으로 설정할 수 있다.
@OneToMany
-
속성 설명 기본값 mappedBy 연관관계의 주인 필드를 선택한다. fetch 글로벌 페치 전략을 설정한다. @ManyToOne
=FetchType.EAGER,@OneToMany
=FetchType.LAZYcascade 영속성 전이 기능을 사용한다. targetEntity 연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 않는다. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.
-
- 일대다 단방향
- 설계 의도
Team
은Team
에 속한Member
들을 추가하거나 수정하고 싶을 때
- 1의 입장인
Team
에서 연관관계의 주인을 관리@Entity public class Team { ... @OneToMany @JoinColumn(name = "TEAM_ID") 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);
- 일반적으로 수행되는
insert
쿼리에 추가로update
쿼리가 실행 된다.- team 엔티티를 저장했지만 연관 관계는 반대편(Member) 테이블을 가리키고 있으므로 반대편 테이블의
update
쿼리가 한번 더 실행됨 - 의도하지 않은 쿼리가 수행되므로 복잡도가 올라가며 및 성능상 좋지 않다.
- team 엔티티를 저장했지만 연관 관계는 반대편(Member) 테이블을 가리키고 있으므로 반대편 테이블의
- 테이블 일대다 관계는 항상 다(N) 쪽에 외래키가 있음.
- 객체와 테이블의 차이 때문에 반대편 테이블의 외래키를 관리하는 특이한 구조가 된다.
@JoinColumn
을 사용하지 않으면 조인 테이블 방식을 사용하므로@JoinColumn
을 꼭 써야한다.- 조인 테이블 방식 : 연관관계를 위해 두 테이블 사이에 중간 테이블을 생성하여 매핑한다 (좋지 않음)
- 일대다 단방향 매핑보다는 다대일 양방향 매핑을 사용하자!
- 설계 의도
- 일대다 양방향
- 설계 의도
Team
은Team
에 속한Member
들을 추가하거나 수정하고 싶고Member
는Member
가 속한 팀을 조회하고 싶을 때
- 공식 스펙상으로 지원하지 않으나 야매 방식(insertable, updatable 속성을 사용하여 읽기전용 필드로 만듬)으로 처리할 수 있음
@Entity public class Member { ... @ManyToOne @JoinColumn(name="TEAM_ID", insertable=false, updatable=false) private Team team; }
@JoinColumn
의 속성으로 해당 필드를 수정하지 못하게 만듦으로 써 read-only로 만듬 (양방향 매핑 구현)- 복잡하게 하지말고 깔끔하게 다대일 양방향을 사용하자.
- 설계 의도
일대일 (1:1)
- 주 테이블이나 대상 테이블 중에 외래키 선택 가능하다.
- 주 테이블에 외래키
- 대상 테이블에 외래키
-
외래키에 데이터베이스 유니크 제약조건을 추가해야 함.
- 주 테이블에 외래키 - 단방향
- 설계 의도
- 각
Member
(주 테이블)는 락커를 하나 소유할 수 있다. - 다대일(
@ManyToOne
) 단방향 매핑과 유사함@Entity public class Locker { @Id @GeneratedValue private Lond id; private String name; } @Entity public class Member { @Id @GeneratedValue private Long id; @OneToOne @JoinColumn(name = "LOCKER_ID") private Locker locker; private String username; }
- 각
- 추후에 하나의
Locker
를 여러명의Member
가 사용할 수 있도록 (다대일) 관계를 확장할 수 있다. (DB 입장) Member
에Locker
가 있는게 성능상으로 유리하다- ex)
Member
가 주 테이블이기 때문에Member
조회는 필수적이다.Member
가 가진Locker
정보를 확인하기 위해 굳이Locker
를 대상으로 한번 더 조회하지 않아도 된다.
- ex)
- 설계 의도
- 주 테이블에 외래키 - 양방향
- 설계 의도
- 각
Member
는 락커를 하나 소유할 수 있고 Locker
는 자신을 소유한Member
를 알수있음.@Entity public class Locker { @Id @GeneratedValue private Lond id; private String name; @OneToOne(mappedBy= "locker") private Member member; } @Entity public class Member { @Id @GeneratedValue private Long id; @OneToOne @JoinColumn(name = "LOCKER_ID") private Locker locker; private String username; }
- 각
- 다대일 양방향 매핑처럼 외래키가 있는곳이 연관관계의 주인
- 예제에선
Member
가 주인
- 예제에선
- 반대편은
mappedBy
를 적용
- 설계 의도
- 주 테이블 외래키 정리
- 주 객체가 대상 객체의 참조를 가지는 것 처럼 주 테이블에 외래키를 두고 대상 테이블을 찾는다.
- 객체지향 개발자가 선호한다.
- JPA 매핑이 편리하다.
- 장점 : 주 테이블만 조회해도 (외래키로) 대상 테이블에 데이터가 있는지 확인 가능하다. (성능상 유리)
- 단점 : 값이 없으면 외래키에 null을 허용한다.
- 대상 테이블에 외래키 - 단방향 (불가능)
Member
엔티티의locker
가 연관관계의 주인으로 설정하고 싶으나 외래키는LOCKER
에 있는 상황- 일대다 단방향 같은 상황
- JPA에서 지원하지 않는다.
- 대상 테이블에 외래키 - 양방향
Locker
에 있는member
를 연관관계 주인으로 설정하고 (LOCKER
테이블에 외래키가 있으므로) 양방향 연관관계를 설정- 반대편인
Member
의locker
를 읽기 전용으로 설정
- 반대편인
- 사실 주 테이블에 외래키 양방향과 매핑 방법은 동일하다.
- 대상 테이블 외래키 정리
- 대상 테이블에 외래키가 존재한다. (null 허용 문제 해결)
- 전통적인 데이터베이스 개발자들이 많이 선호한다.
- 장점 : 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조가 유지된다. (확장에 유리)
- 단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩된다.
- JPA 입장에서는
Member
객체를 로딩할 때 외래키 대상인locker
에 값이 있는지 없는지 알아야함. - 그러나 외래키는 반대편인
LOCKER
가 관리하기 때문에MEMBER
테이블 말고도LOCKER
테이블까지 전부 조회 해봐야함. - 결국 주 테이블 과 대상 테이블 둘다 확인하는 과정을 필히 거치므로 지연 로딩이 아무 의미 없음. (그래서 항상 즉시 로딩이 됨)
- 지연 로딩 : 실제 객체를 사용하는 시점에 조회
- 즉시 로딩 : Join을 활용하여 연관된 객체까지 한번에 조회
- JPA 입장에서는
다대다 (N:M) - 권장하지 않음
- 관계형 데이터베이스
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
- 연결 테이블을 추가해서 일대다 - 다대일 관계로 풀어내야 함.
- 객체
- 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
- 단방향, 양방향 둘다 가능하다.
@ManyToMany
를 사용한다.@JoinTable
로 연결 테이블을 지정할 수 있다.@Entity public class Product { @Id @GeneratedValue private Long id; private String name; @ManyToMany(mappedBy = "products") private List<Member> members = new ArrayList<>(); // getter, setter } @Entity public class Member { @Id @GeneratedValue private Long id; private String name; @ManyToMany @JoinTable(name = "MEMBER_PRODUCT") private List<Product> products = new ArrayList<>(); // getter, setter }
- 다대다 매핑의 한계
- 편리해 보이지만 실무에서 사용하면 안됨.
- 연결 테이블이 단순히 연결만 하고 끝나지 않기 때문
- 주문시간, 수량 같은 데이터가 들어올 수 있음.
- 매핑 정보만 있어야 함.
- 중간에 연결 테이블이 존재해서 쿼리를 예측할 수 없음
- 다대다 매핑 극복
- 연결 테이블 용 엔티티를 추가 (연결 테이블을 엔티티로 승격)
@ManyToMany
->@OneToMany
,@ManyToOne
@Entity public class Product { @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedBy = "product") private List<MemberProduct> memberProducts = new ArrayList<>(); // getter, setter } @Entity public class MemberProduct { @Id @GeneratedValue private Long id; @ManyToOne @JoinColumn(name = "MEMBER_ID") private Member member; @ManyToOne @JoinColumn(name = "PRODUCT_ID") private Product product; } @Entity public class Member { @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedBy = "member") private List<MemberProduct> memberProducts = new ArrayList<>(); // getter, setter }
📣 Reference
본 포스팅은 김영한님의 강의를 듣고 스스로 정리 및 추가한 내용입니다.
댓글남기기