공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
다대일 [N:1]
연관관계 매핑 시 고려사항 3가지
1. 다중성
- 다대일 : @ManyToOne
- 일대다 : @OneToMany
- 일대일 : @OneToOne
- 다대다 : @ManyToMany (실무에서 사용하면 안 된다.)
2. 단방향, 양방향
- 테이블
- 외래 키 하나로 양쪽 조인 가능하다.
- 사실 방향이라는 개념이 없다.
- 객체
- 참조용 필드가 있는 쪽으로만 참조 가능하다.
- 한쪽만 참조하면 단방향이다.
- 양쪽이 서로 참조하면 양방향이다.
3. 연관관계의 주인
- 테이블은 외래 키 하나로 두 테이블이 연관관계를 맺는다.
- 객체 양방향 관계는 A->B, B->A처럼 양쪽에서 참조가 가능하다.
- 객체 양방향 관계에서 테이블의 외래 키를 관리할 곳을 지정해야 한다.
- 연관관계의 주인은 외래 키를 관리하는 참조이다.
- 주인의 반대편은 외래 키에 영향을 주지 않으며, 단순 조회만 가능하다.
다대일 단방향
- 가장 많이 사용하는 연관관계이다.
- 다대일의 반대는 일대다.
다대일 양방향
- 외래 키가 있는 쪽이 연관관계의 주인이다.
- 양쪽을 서로 참조하도록 개발한다.
일대다 [1:N] 단방향
- 1:N 중 1이 연관관계 주인이다.
- 객체 입장에선 논리적으로 1(Team)에 FK가 들어갈 수 있지만 실제 DB에선 무조건 N(Member)에 FK가 들어가야 한다. 왜냐하면 TEAM_ID는 유일해야 하는데 FK로 잡아버리면 중복이 되기 때문이다.
- 어쨌든 객체 연관관계에서는 Team에 FK를 주는 게 가능하다. 하지만 그렇게 되면 List members 값을 변경했을 때 다른 테이블에 있는 FK(TEAM_ID)를 관리해야 한다.
@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 class JpaMain {
public static void main(String[] args) {
//생략
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();
}
}
- insert 쿼리 두 번에 update 쿼리가 추가로 수행되었다.
- 양쪽 객체를 저장하는 insert 쿼리 후에 team.getMembers(). add(member); 여기서 team 엔티티를 저장하려면 Member 테이블을 update 할 수밖에 없는 구조이다.
- 따라서 어쩔 수 없이 쿼리가 한 번 더 수행된다.
또한 1:N 방식은 여러 테이블이 엮여 있는 실무에서 문제를 일으킬 수 있다.
Team에 데이터를 insert 하면 연관된 다른 테이블에도 update가 발생할 수 있어 복잡해진다.
따라서 객체지향적으로 조금 손해(ex) Member에서 Team으로 갈 일이 없는데 만드는 것)를 보더라도 이 방식은 권장하지 않는다.
정리
- 일대다 단방향은 1:N 관계에서 1이 연관관계의 주인이다.
- 테이블의 일대다 관계에서는 항상 N 쪽에 외래 키가 있다.
- 객체와 테이블의 차이로 인해 반대편 테이블의 외래 키를 관리하는 특이한 구조이다.
- @JoinColumn을 반드시 사용해야 하며, 그렇지 않으면 중간에 테이블이 추가되는 조인 테이블 방식을 사용하게 된다.
- 1:N 단방향 매핑보다는 N:1 양방향 매핑을 사용하자.
일대다 [1:N] 양방향
- Member 입장에서 Team의 정보가 필요할 때 사용한다.
- 공식적인 스펙상 가능하지 않지만, 편법으로는 가능하다.
@Entity
public class Member {
//생략
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team;
}
- 이렇게 insertable = false, updatable = false을 넣으면 매핑도 되어있고 값도 다 쓰는데, 읽기 전용이 된다.
정리
- 이런 매핑은 공식적으로 존재하지 않는다.
- @JoinColumn(insertable=false, updatable=false)를 사용한다.
- 읽기 전용 필드를 통해 양방향처럼 사용하는 방법이다.
- 권장 방식은 다대일 양방향 매핑을 사용하는 것이다.
일대일 [1:1]
일대일 단방향
- 일대일 관계는 양쪽 테이블 모두 일대일 관계를 가질 수 있다.
- 외래 키를 주 테이블 또는 대상 테이블에 선택하여 저장할 수 있다.
ㄴ 주 테이블에 외래 키를 두는 방식
ㄴ 대상 테이블에 외래 키를 두는 방식
- 외래 키에 데이터베이스 유니크(UNI) 제약 조건을 추가하여 유일성을 보장한다.
- 다대일 단방향 매핑과 유사하다.
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
}
@Entity
public class Member {
//생략
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
}
일대일 양방향
- 다대일 단방향 매핑처럼 외래 키가 있는 곳이 연관관계 주인이다.
- 반대편은 mappedBy를 적용한다.
@Entity
public class Locker {
@Id
@GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "locker") // 읽기전용
private Member member;
}
일대일 : 대상 테이블에 외래 키 단방향(불가능)
- 이 방식은 JPA에서 지원하지 않는다.
- 대상 테이블에 외래키를 주고 싶으면 양방향으로 설계해야 한다.
일대일 : 대상 테이블에 외래 키 양방향
- 사실 이 방식은 일대일 방식을 대칭으로 뒤집은 것과 같다.
일대일 정리
주 테이블에 외래 키
- 주 객체가 대상 객체를 참조하듯이, 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
- 객체지향 개발자가 선호한다.
- JPA 매핑이 편리하다.
- 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인 가능하다.
- 단점: 값이 없으면 외래 키에 null을 허용해야 한다.
대상 테이블에 외래 키
- 대상 테이블에 외래 키가 존재한다.
- 전통적인 데이터베이스 개발자가 선호한다.
- 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때 테이블 구조를 유지할 수 있다.
- 단점: 프록시 기능의 한계로 인해 지연 로딩으로 설정해도 항상 즉시 로딩된다.
요약
- 주 테이블에 외래 키를 두면 객체지향적으로 매핑이 편리하지만, 외래 키에 null을 허용해야 한다.
- 대상 테이블에 외래 키를 두면 전통적인 데이터베이스 개발에 유리하지만, 항상 즉시 로딩된다.
다대다 [N:M]
결론부터 말하면 실무에서는 쓰면 안 되는 방식이다.
- 관계형 데이터베이스는 정규화된 테이블 2개로 N:M 관계를 표현할 수 없다.
- 연결 테이블을 추가해서 1:N, N:1 관계로 풀어내야 한다.
하지만 객체는 가능하다.
- Member도 Product List를 가질 수 있고 Product도 Member List를 가질 수 있다.
Product 엔티티
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
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;
}
}
Member 엔티티
@Entity
public class Member {
//생략
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
}
- @ManyToMany를 사용한다.
- @JoinTable에 연결 테이블명을 지정한다.
- 연결 테이블을 생성하고 외래키 제약조건도 생성한다.
양방향으로 매핑하고 싶으면 Product에 mappedBy를 써주면 된다.
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
}
다대다 매핑의 한계
- 편리해 보이지만 실무에서 사용할 수 없다.
- 연결 테이블이 단순히 연결만 하고 끝나지 않는다.
- 중간 테이블에 주문시간, 수량 같은 추가 데이터를 넣을 수 없다.
- 중간 테이블이 숨겨져 있어 쿼리가 복잡하게 작성된다.
다대다 한계 극복
- 연결 엔티티를 하나 추가한다.
- @ManyToMany -> @OneToMany, @ManyToOne
@Entity
public class Member {
//생략
@OneToMany(mappedBy = "member")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
@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 Product {
// 생략
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
}
실전 예제 3 - 다양한 연관관계 매핑
배송, 카테고리 추가 - 엔티티
- 주문과 배송은 1:1 관계이다. (@OneToOne)
- 상품과 카테고리는 N:M 관계이다. (@ManyToMany)
배송, 카테고리 추가 - ERD
- 주문과 배송은 1:1 관계라서 DELIVERY_ID (FK)를 어디에 넣을지 고민해야 하는데, 주 테이블에 넣기로 결정했다.
- 카테고리와 아이템은 N:M 관계이다.
배송, 카테고리 추가 - 엔티티 상세
Order와 Delivery를 1대1 양방향으로 설정한다.
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
@OneToOne
@JoinColumn(name = "DELIVERY_ID")
private Delivery delivery;
}
@Entity
public class Delivery {
@Id @GeneratedValue
private Long id;
private String city;
private String street;
private String zipcode;
private DeliveryStatus status;
@OneToOne(mappedBy = "delivery")
private Order order;
}
다음으로 카테고리와 아이템을 설계한다.
하나의 카테고리에 여러 아이템이 소속될 수 있고,
하나의 아이템에 여러 카테고리가 소속될 수 있다고 가정한다.
@Entity
public class Category {
@Id
@GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> chlid = new ArrayList<>();
@ManyToMany
@JoinTable(name = "CATEGORY_ITEM",
joinColumns = @JoinColumn(name = "CATEGORY_ID"), //조인 테이블에서 현재 엔티티에 대한 외래키를 지정
inverseJoinColumns = @JoinColumn(name = "ITEM_ID") //조인 테이블의 반대쪽 엔티티에 대한 외래키 설정
)
private List<Item> items = new ArrayList<>();
}
@Entity
public class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categoryList = new ArrayList<>();
//생략
}
이제 실행해 보면
의도한 대로 테이블이 생성된 것을 확인할 수 있다.
N:M 관계는 1:N, N:1로
- 위의 코드는 예시이기 때문에 N:M을 쓴 것이고, 실무에서는 사용하면 안 된다.
- 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1 관계로 표현된다.
- 실전에서는 중간 테이블이 단순하지 않다.
- @ManyToMany는 제약이 많다: 필드 추가 불가, 엔티티와 테이블 간 불일치 문제 발생.
- 실전에서는 @ManyToMany 사용을 권장하지 않는다.
@JoinColum
- 외래 키를 매핑할 때 사용한다.
@ManyToOne
- 다대일 관계 매핑할 때 사용한다.
- mappedBy가 없다.
@OneToMany
- 일대다 관계 매핑할 때 사용한다.
- mappedBy가 있다.
즉 다대일은 연관관계 주인이 되어야 한다.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 프록시와 연관관계 관리 (0) | 2024.07.16 |
---|---|
[JPA] 고급 매핑 (0) | 2024.07.15 |
[JPA] 연관관계 매핑 기초 (2) | 2024.07.14 |
[JPA] 엔티티 매핑 (0) | 2024.07.12 |
[JPA] 영속성 관리 - 내부 동작 방식 (1) | 2024.07.12 |