2021. 2. 5. 16:58ㆍ프로그래밍 언어/Spring Framework
[인프런 김영한] JPA 연관관계 - 다양한 연관관계 (N:1, 1:N, 1:1, N:M)
해당 글은 인프런 김영한강사님의 영상을 보고 정리한 글입니다.
Spring Boot, Spring Data JPA를 사용해 실습하였습니다.
김영한 인프런 : www.inflearn.com/users/@yh
▣ Goal
1. 연관관계 매핑시 고려할 사항
2. 다대일[N:1] (단방향, 양방향)
3. 일대다[1:N] (단방향, 양방향)
4. 일대일[1:1] (단방향, 양방향)
5. 다대다[N:N] (단방향, 양방향)
▣ 연관관계 매핑시 고려사항 3가지
1. 다중성
| 데이터베이스의 관점으로 다중성을 생각하면 된다.
- @ManyToOne
- @OneToMany
- @OneToOne
- @ManyToMany -> 실무에서 사용 X
2. 단방향, 양방향
* 테이블
- 외래키 하나로 양쪽 조인 가능
- 사실 방향이라는 개념이 없음
* 객체
- 참조용 필드가 있는 쪽으로만 참조 가능
- 한쪽만 참조하면 단방향, 양쪽이 서로 참조(단방향(->) + (<-)단방향)하면 양방향
3. 연관관계의 주인
* 테이블은 외래키 하나로 두 테이블이 연관관계를 맺음
* 객체 양방향 관계는 참조가 2군데 있으며 둘중 테이블의 외래키를 관리할 곳을 지정해야함
* 연관관계의 주인 : 외래키를 관리하는 잠조
* 주인의 반대편 : 외래키에 영향을 주지 않음, 단순 조회만 가능
lombok을 사용하였으며
@Entity, @Getter, @Setter는 포함하지 않았습니다.
▣ 다대일[N:1] - 단방향
public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String username; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; } |
public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; |
( N : 1 )
* N 쪽에 외래키(Team team)가 있도록 설계해야 한다.
▣ 다대일[N:1] - 양방향
public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String username; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; } |
public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); } |
( N : 1 )
* Team에 member를 참조하고 있지만 테이블에는 전혀 영향이 없다.
* 외래키가 있는 쪽이 연관관계의 주인(Member)
* 양쪽을 서로 참조하도록 개발
▣ 일대다[1:N] - 단방향
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 class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String username; } |
// 실행문 Member member = new Member(); member.setUsername("member1"); memberRepository.save(member); Team team = new Team(); |
( 1 : N )
* 객체 연관관계에서는 Team이 주인이 될 수 있다.
하지만 결국에는 DB관점에서 봐야 하는데 테이블 연관관계에서의 FK는 Member에 외래키가 들어갈수밖에 없다.
* 실행은 된다. DB를 확인하면 정상적으로 데이터가 들어온것을 확인할 수도 있다.
하지만 아래 Update쿼리가 추가적으로 실행이 되었다. -> 성능상의 단점
update
member
set
team_id=?
where
member_id=?
'team.getMembers().add(member);'
결국 Team에서 members가 바뀌면 DB의 Member에 업데이트 쿼리가 실행이 된다.
Team Entity에서 Members를 참조 한다. 하지만 테이블 관계로 보았을 때Member 테이블에 FK가 있기 때문에 업데이트 쿼리가 나가는 것이다.-> Entity가 관리하는 외래키가 다른 테이블에 있다.
* @JoinColumn를 적지 않으면 조인 테이블 방식을 사용하며, 중간에 테이블이 생성이 된다.테이블이 하나 더 추가되고 신경써야 하기 때문에 1:N을 사용한다면 꼭 넣어주자.
반대로 조회할 일이 없다고 해도, 가급적 다대일[N:1] 양방향을 사용하자
▣ 일대다[1:N] - 양방향
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 class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String username; @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) private Team team; } |
( 1 : N )
* 일대다 양방향은 공식적으로 존재하지 않는다.* 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법이며 권장하지 않는다.
* insertable = false, updatable = false = false로 하면 읽기 전용이 된다. 참조 Entity를 읽기 전용으로 매핑한다.- insertable = false : 저장시 해당 필드도 함께 저장되며, false로 설정하면 DB에 저장하지 않는다.- updatable = false : 업데이트시 해당 필드도 함께 업데이트되며, false로 설정하면 DB에 업데이트하지 않는다.
▣ 일대일[1:1] - 주 테이블에 외래키 단방향※ 주 테이블 : 많이 Access하는 테이블을 지정
public class Locker { @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; } |
// 주 테이블 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; @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) private Team team; } |
( 1 : 1 )
* 주 테이블이나 대상 테이블 중에 외래키 선택 가능* 외래키에 데이터베이스 유니크 제약조건 추가
create table member (
member_id bigint not null,
username varchar(255),
locker_id bigint,
team_id bigint,
primary key (member_id)
)
| 실행해 보면 member Table에 locker가 들어가 있는것을 확인할 수 있다.
▣ 일대일[1:1] - 주 테이블에 외래키 양방향
public class Locker { @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; // 읽기 전용 @OneToOne(mappedBy = "locker") private Member member; } |
// 주 테이블 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; @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) private Team team; } |
( 1 : 1 )
* 다대일 양방향 매핑처럼 외래키가 있는 곳이 연관관계의 주인
▣ 일대일[1:1] - 대상 테이블에 외래키 단방향
* 단방향 관계는 JPA에서 지원하지 않는다.
▣ 일대일[1:1] - 대상 테이블에 외래키 양방향
public class Locker { @Id @GeneratedValue @Column(name = "LOCKER_ID") private Long id; private String name; } |
// 주 테이블 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; @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) private Team team; } |
( 1 : 1 )
* 일대일[1:1] 주 테이블에 외래키 양방향과 매핑 방법은 같다.
◈ 일대일 관계에서 외래키를 어디에 넣어야 할까 (Member와 Table 예시)
DBA 관점 | Locker에 FK를 넣는게 차후 변경에 용이하다.* 현재는 1명의 Member가 1개의 Locker를 가질 수 있는데 차후
1명의 Member가 N개의 Locker를 가질 수 있다고 변경할 때 테이블 구조를 변경할 필요가 없다.
단점 : 프록시 기능의 한계로 지연 로딩으로 설정해도 항상 즉시 로딩됨
개발자 관점 | Member에 FK를 넣는게 성능상 좋다. , JPA 매핑이 편하다.
* Member 테이블을 조회할 일이 많다면 Locker 값도 함께 가지고 온다. 조인이 필요가 없고 조인도 필요가 없다.
주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 가능하다.
단점 : 값이 없드면 외래키에 null을 허용하게 된다.
* 다대다는 기본적으로 실무에서 사용 하지 않기를 권장하는 연관관계이다. *
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없기 때문에
연결 테이블을 중간에 추가해서 다대일 관계로 풀어내는게 바람직한 해결 방법이다.
▣ 다대다[N:M] - 단방향
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; @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) private Team team; @ManyToMany @JoinTable(name = "MEMBER_PRODUCT") private List<Product> products = new ArrayList<>(); } |
public class Product { @Id @GeneratedValue private Long id; private String name; } |
Hibernate: create table member ( member_id bigint not null, username varchar(255), locker_id bigint, team_id bigint, primary key (member_id) ) Hibernate: create table member_product ( member_member_id bigint not null, products_id bigint not null ) Hibernate: create table product ( id bigint not null, name varchar(255), primary key (id) ) |
| @ManyToMany 어노테이션을 지정, JoinTable에 생성될 테이블 이름을 적어주면 다대다 단방향 구조가 된다.
실제 쿼리문도 create table member_product를 자동으로 생성하게 됨.
▣ 다대다[N:M] - 양방향
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; @ManyToOne @JoinColumn(name = "TEAM_ID", insertable = false, updatable = false) private Team team; @ManyToMany @JoinTable(name = "MEMBER_PRODUCT") private List<Product> products = new ArrayList<>(); } |
public class Product { @Id @GeneratedValue private Long id; private String name; @ManyToMany(mappedBy = "products") private List<Member> members = new ArrayList<>(); } |
| 다대다 양방향일경우 Product 쪽에 @ManyToMany 어노테이션에 mappedBy를 적어주면
다대다 양방향이 완성된다.
◈ 다대다 매핑의 한계와 극복
[한계]
▶ 실무에서는 사용할수가 없는데 그 이유로는
- 연결 테이블이 단순히 연결만 하고 끝나지 않음
- 연결 테이블에 주문시간, 수량같은 데이터가 들어올 수 있음
- 연결 테이블로 인해 생각하지 못한 쿼리가 나갈 수 있다.
[극복]
▷연결 테이블용 Entity를 추가(연결 테이블을 Entity로 승격)
Member [N:M] Product -> Member [1:N] Order [N:1] Product
public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String username; @OneToMany(mappedBy = "member") private List<MemberProduct> memberProducts = new ArrayList<>(); } |
public class MemberProduct { @Id @GeneratedValue private Long id; @ManyToOne @JoinColumn(name = "MEMBER_ID") private Member member; @ManyToOne @JoinColumn(name = "PRODUCT_ID") private Product product; } |
public class Product { @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedBy = "product") private List<MemberProduct> memberProducts = new ArrayList<>(); } |
| 다대다 개념을 사용하고 싶으면 기존에 자동으로 생성되었던 연결 테이블을 MemberProduct라고 클래스를 생성해서 Entity로 만들어 사용한다.
중간에 테이블이 하나 더 생기기 때문에 OneToMany와 ManyToOne으로 변경해서 사용하는게 훨씬 유연하다.
'프로그래밍 언어 > Spring Framework' 카테고리의 다른 글
[인프런 김영한] JPA - 기본키 매핑 방법(@Id, @GeneratedValue) (0) | 2021.02.07 |
---|---|
[인프런 김영한] JPA - 데이터베이스 스키마 자동생성 (0) | 2021.02.07 |
[인프런 김영한] 연관관계 매핑 기초 - 단방향 연관관계 (0) | 2021.02.04 |
[인프런 김영한] 연관관계 매핑 기초 - 양방향 연관관계 매핑시 가장 많이 하는 실수 (0) | 2021.02.04 |
[인프런 김영한] 연관관계 매핑 기초 - 양방향 연관관계 (0) | 2021.02.03 |