6.1 다대일 (n : 1)
다대일 단방향
- 다대일 단방향은 가장 많이 사용하는 연관관계이다.
- 다대일의 반대는 일대다
Ex) 다대일 단방향
- Member("다", 주인 쪽)에만 아래와 같이 표시된다.
- Team("일") 쪽에는 아무 표시없다.
<hide/>
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team; // 연관 관계의 주인
다대일 양방향
- 반대 쪽에도 추가한다.
- 양 쪽을 서로 참조하도록 개발한다.
- 외래 키가 있는 쪽(MEMBER)이 연관 관계의 주인이다.
- 테이블에 전혀 영향을 주지 않는다. (주인이 아니라서)
Ex) 다대일 양방향
- Team 클래스에도 @OneToMany를 추가한다.
- "mappedBy"가 있으면 가짜 주인 클래스 => (mappedBy ="team")
- mappedBy는 자신이 연관관계의 주인이 아님을 뜻한다.
- 주인 쪽 Member클래스에서 Team의 변수명이 team이므로 괄호안에 team을 넣어준다.
- Team 객체에서 특정 team에 속하는 모든 member를 조회하려면 List<Member> 객체가 필요하다.
- Team =>One, List => Many
<hide/>
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
6.2 일대다 (1 : n)
일대다 단방향
- "일" 에 해당하는 Team에 대해 연관 관계를 매핑한다.
- 팀은 멤버를 알고 싶은데 멤버는 팀을 모르게 하려면?
- 그런데, 데이터베이스에는 무조건 "다" 에 해당하는 Member가 테이블로 들어가야한다.
- 일대다 단방향은 일대다에서 "일" 에 해당하는 Team이 연관관계의 주인이다.
- 테이블 일대다 관계는 항상 "다" 쪽에 외래 키가 있다.
- 객체와 테이블의 차이 때문에 반대편 테이블(MEMBER)의 외래 키를 관리하는 구조가 특이하다.
- @JoinColumn을 꼭 사용해야한다. 그렇지 않으면 조인 테이블 방식을 사용한다. (중간에 테이블을 하나 추가)
- Tip: 강사님은 실무에서
일대다 단방향을 쓰지 않는다. - 운영이 어려워진다. 다대일 단방향 쓰다가 필요하면 양방향으로 쓰는 것을 권장한다.
- Tip: 강사님은 실무에서
- 일대다 단방향의 단점: 엔티티가 관리하는 외래 키가 다른 테이블에 있다.
- 따라서, 일대다 단방향은 연관 관계 관리를 위해 추가로 UPDATE 쿼리를 실행한다.
Ex) 일대다 단방향
- 연관 관계를 반대 쪽에 넣어야한다. Team에 join 컬럼을 넣는다.
- Team 클래스에 다음과 같이 추가한다.
- 그런데, 여기서 Team의 변수 id의 name과 같아도 되는건가?
<hide/>
@OneToMany
@JoinColumn(name ="TEAM_ID")
private List<Member> members = new ArrayList<>();
- 이 부분은 팀 테이블에 insert 될 수 있는 내용이 아니다.
- 연관 관계가 멤버 테이블에 있기 때문이다.
- 따라서, 연관 관계의 주인 Member 테이블의 team_id를 업데이트 해줘야한다.
team.getMembers().add(member);
<hide/>
package hellojpa;
import java.util.List;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
public class JpaMain {
public static void main(String[] args) {
EntityManagerFactory emf =
Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
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();
}catch (Exception e) {
tx.rollback();
}finally {
em.close();
}
emf.close(); // 팩토리를 나중에 닫는다.
}
}
Note) 실행 결과
- create table Team하고나서 update 쿼리를 실행한다.
- 왜 update가 실행될까?
- em.persist(team): team 엔티티를 저장하는데 team_id를 저장하려면 member 테이블을 업데이트 시키는 수밖에 없다.
- 팀을 수정했는데 멤버가 업데이트된다.
Ex) 위 예제의 Team 클래스에 @JoinColumn 애너테이션을 주석 처리하고 실행
<hide/>
@OneToMany
// @JoinColumn(name ="TEAM_ID")
private List<Member> members = new ArrayList<>();
Note) 실행 결과
- TEAM_MEMBER라는 중간 테이블이 생성된다.
- @JoinColumn을 붙이면 조인 테이블에 대한 설정 정보를 넣을 수 있다.
- 그런데, 테이블이 하나 더 생기면 성능, 운영상 좋은 편은 아니다.
일대다 양방향 - 비공식
- 공식적으로 존재하는 방법은 아니다.
- 기존의 일대다 단방향 방식에서 멤버 클래스가 팀 클래스를 참조 가능하며 MEMBER 테이블 읽기 전용으로 매핑된다.
- @JoinColumn(insertable = false, updatable = false)
- 읽기 전용 필드를 사용해서 양방향처럼 사용하는 방법이다.
- 위에서 Team의 members가 연관 관계의 주인이다.
- 그런데 Member에서 Team을 조회하려면?
Ex) 일대다 양방향
- Member 클래스에 다음과 같은 필드를 추가
- 그런데 이렇게 해버리면 연관 관계의 주인이 2개가 되버린다.
<hide/>
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team; // 연관 관계의 주인
Note) 실행 결과 - 오류가 난다.
- 오류를 막기 위해 Member 클래스의 team에 대해 @JoinColumn() 안에 옵션을 넣어준다.
- 그러면 읽기 전용이 되버린다.
- 매핑은 되어 있으나 insert, update는 되지 않도록 한다.
<hide/>
@ManyToOne
@JoinColumn(name = "TEAM_ID", insertable = false, updatable = false)
private Team team; // 연관 관계의 주인
6.3 일대일 (1: 1)
일대일 관계
- 일대일 관계는 그 반대도 일대일이다.
- 주 테이블이나 대상 테이블 중에 외래 키를 선택 가능하다.
- 주 테이블에 외래 키
- 대상 테이블에 외래 키
- 외래 키에 데이터베이스 유니크(UNI) 제약 조건을 추가한다.
일대일 - 주 테이블에 외래 키 단방향
- 다대일 단방향 매핑과 유사하다. 어노테이션만 다르다.
- 다대일 양방향 매핑처럼 외래 키가 있는 곳이 연관 관계의 주인이다. 반대편은 mappedBy 적용
- ex) 회원 - 사물함의 관계
cf) 다대일 단방향과 유사하다.
Ex) 일대일
- 새로운 클래스 Locker를 만들고 멤버 테이블에 locker를 추가한다.
<hide/>
package hellojpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
@Entity
public class Locker {
@Id @GeneratedValue
private Long id;
private String name;
@OneToOne(mappedBy = "locker") // 멤버 테이블에 있는 변수 locker
private Member member;
}
<hide/>
@OneToOne
@JoinColumn(name = "LOCKER_ID")
private Locker locker;
일대일 - 대상 테이블에 외래 키 단방향
- 멤버의 locker를 연관관계 주인이라 하고 싶은데 외래 키가 LOCKER에 있다?
- Member의 locker로 LOCKER의 MEMBER_ID를 관리할 수 있을까? => 불가능하다.
- 대상 테이블에서의 단방향 관계는 JPA에서 지원하지 않는다.
- 양방향 관계는 지원한다.
일대일 - 대상 테이블에 외래 키 양방향
- Locker의 member를 연관 관계 주인으로 잡고 LOCKER 테이블과 매핑한다.
- (일대일 - 주 테이블에 외래 키 양방향) 을 뒤집은 모양이다.
- 일대일 관계는 내가 내 것만 관리 가능하다.
일대일 정리
- 주 테이블에 외래 키
- 주 객체가 대상 객체의 참조를 가지는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 찾는다.
- 객체 지향 개발자가 선호한다.
- JPA 매핑이 편리하다.
- 장점: 주 테이블만 조회해도 대상 테이블에 데이터가 있는지 확인이 가능하다.
- 단점: 값이 없으면 외래 키에 null을 허용한다.
- 대상 테이블에 외래 키
- 대상 테이블에 외래 키가 존재한다.
- 전통적인 데이터베이스 개발자가 선호한다.
- 장점: 주 테이블과 대상 테이블을 일대일에서 일대다 관계로 변경할 때, 테이블 구조를 유지한다.
- 단점: 프록시 기능의 한계로 지연 로딩으로 설정해서 항상 즉시 로딩된다.
- 프록시: 지연 로딩으로 설정했을 때 연관된 엔티티가 있으면 프록시 객체가 대신 들어가면 되지만 연관된 엔티티가 없으면 null이 들어가야한다.
6.4 다대다 (n : m)
다대다 - 실무에서 쓰지 않도록 한다.
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
- 중간 역할을 하는 연결 테이블(Member_Product)을 추가해서 일대다, 다대일 관계로 풀어내야한다.
- @ManyToMany
- @JoinTable로 연결 테이블을 지정한다.
- 다대다 매핑: 단방향, 양방향 가능하다.
- 관계형데이터베이스와 다르게 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계가 가능하다.
Ex) 다대다 매핑 (member - product)
- product 클래스
<hide/>
package hellojpa;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@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클래스의 products 변수에 중간 테이블 이름 "member_product"을 넣어준다.
<hide/>
@ManyToMany
@JoinTable(name = "MEMBER_PRODUCT")
private List<Product> products = new ArrayList<>();
Note) 실행 결과
- 중간 테이블 member_product 테이블이 생긴다.
- 다음과 같이 외래 키 제약 조건도 생긴다.
- MemberProduct테이블의 product (컬럼명: products_id)
- MemberProduct테이블의 member (컬럼명: MEMBER_ID)
- 양방향으로 만드려면?
- Product 클래스에 다음과 같이 추가한다.
<hide/>
@ManyToMany(mappedBy = "products")
private List<Member> members = new ArrayList<>();
다대다 매핑의 한계
- 편리해 보이지만 실무에서는 사용하지 않는다.
- 연결 테이블이 단순히 연결만하고 끝나지 않는다.
- 중간 테이블이 숨겨져 있어서 생각치 못한 쿼리가 나온다.
- 주문 시간, 수량 같은 데이터가 들어올 수 있다.
- PK이면서 FK 를 만족하도록 잡는다.
- 즉, (MEMBER_ID + PRODUCT_ID): PK
- MEMBER_ID와 PRODUCT_ID 각각은 FK
다대다 한계 극복
- 연결 테이블용 엔티티를 추가한다. (연결 테이블을 엔티티로 승격)
- @ManyToMany => @OneToMany, @ManyToMany
Ex)
- 연결용 테이블 MembeProduct를 만든다.
<hide/>
package hellojpa;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
@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;
}
- Product 수정 - List 안의 구성 요소를 수정한다.
<hide/>
@OneToMany(mappedBy = "product")
private List<MemberProduct> memberProducts = new ArrayList<>();
6.5 실전 예제 3 - 다양한 연관 관계 매핑
Ex) JPA-SHOP
- 배송, 카테고리 추가 - Entity
- 주문과 배송은 1: 1 (@OneToMany)
- 상품과 카테고리는 N : M (@ManyToMany)
- 카테고리
<hide/>
package jpabook.jpashop.domain;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
@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> child = new ArrayList<>();
@ManyToMany
@JoinTable(name ="CATEGORY_ITEM",
joinColumns = @JoinColumn(name ="CATEGORY_ID"),
inverseJoinColumns = @JoinColumn(name = "ITEM_ID")
)
// 중간 테이블을 만든다. 내가 조인하는 건 CATEGORY_ID, 반대편 조인은 ITEM_ID
private List<Item> items = new ArrayList<>();
}
- 배송
<hide/>
package jpabook.jpashop.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Delivery {
@Id
@GeneratedValue
private Long id;
private String city;
private String street;
private String zipcode;
private DeliveryStatus status;
}
- 배송, 카테고리 추가 - ERD
- item 클래스
<hide/>
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
- order 클래스
<hide/>
@OneToOne
@JoinColumn(name ="DELIVERY_ID")
private Delivery delivery;
- 배송, 카테고리 추가 - Entity 상세
Note) 실행 결과
<hide/>
Hibernate:
create table Category (
id bigint not null,
name varchar(255),
PARENT_ID bigint,
primary key (id)
)
Hibernate:
create table CATEGORY_ITEM (
CATEGORY_ID bigint not null,
ITEM_ID bigint not null
)
Hibernate:
create table Delivery (
id bigint not null,
city varchar(255),
status integer,
street varchar(255),
zipcode varchar(255),
primary key (id)
)
Hibernate:
create table Item (
ITEM_ID bigint not null,
name varchar(255),
price integer not null,
stockQuantity integer not null,
primary key (ITEM_ID)
)
Hibernate:
create table Member (
MEMBER_ID bigint not null,
city varchar(255),
name varchar(255),
street varchar(255),
zipcode varchar(255),
primary key (MEMBER_ID)
)
Hibernate:
create table OrderItem (
ORDER_ITEM_ID bigint not null,
count integer not null,
orderPrice integer not null,
ITEM_ID bigint,
ORDER_ID bigint,
primary key (ORDER_ITEM_ID)
)
Hibernate:
create table ORDERS (
ORDER_ID bigint not null,
orderDate timestamp,
status varchar(255),
DELIVERY_ID bigint,
MEMBER_ID bigint,
primary key (ORDER_ID)
)
Hibernate:
create table Team (
MEMBER_ID bigint not null,
city varchar(255),
name varchar(255),
street varchar(255),
zipcode varchar(255),
primary key (MEMBER_ID)
)
Hibernate:
alter table Category
add constraint FK8tepc1qkmluodspg6tnliwhit
foreign key (PARENT_ID)
references Category
Hibernate:
alter table CATEGORY_ITEM
add constraint FKf1uerpnmn49vl1spbbplgxaun
foreign key (ITEM_ID)
references Item
Hibernate:
alter table CATEGORY_ITEM
add constraint FKjip0or3vemixccl6vx0kluj03
foreign key (CATEGORY_ID)
references Category
Hibernate:
alter table OrderItem
add constraint FKabge9eqalspcejij53rat7pjh
foreign key (ITEM_ID)
references Item
Hibernate:
alter table OrderItem
add constraint FKk7lmf97wukpquk6d8blxy5neq
foreign key (ORDER_ID)
references ORDERS
Hibernate:
alter table ORDERS
add constraint FKdbs21f1yi0coxy9y0kxw4g9jf
foreign key (DELIVERY_ID)
references Delivery
Hibernate:
alter table ORDERS
add constraint FKh0db7kqr88ed8hqtcqw3jkcia
foreign key (MEMBER_ID)
references Member
- 의도한대로 CATEGORY_ITEM 테이블이 만들어진다.
- ORDERS 테이블에도 DELIVERY_ID 필드가 들어간다.
- 주문할 때 배송지 정보를 추가하려면 연관 관계 편의 메서드 추가하면 된다.
N : M 관계 보다는 1 : N 또는 N : 1로
- 테이블의 N : M 관계는 중간 테이블을 이용해서 1 : N 또는 N : 1로 만든다.
- 실전에서는 중간 테이블이 단순하지 않다.
- @ManyToMany는 제약이 있다.
- 실전에서는 @ManyToMany 사용하지 않는다.
@JoinColumn 속성 - (기본값)
- @JoinColumn : 외래 키를 매핑할 때 사용 - (필드명 + _ + 참조하는 테이블의 기본 키 컬럼명)
- name: 매핑할 외래 키 이름 - (참조하는 테이블의 기본 키 컬럼명)
- referencedColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명
- foreignKey(DDL): 외래 키 제약 조건을 직접 지정 가능하다. 테이블 생성 시에만 사용한다.
- unique / nullable insertable / updateable / columnDefinition /table: @Column의 속성과 같다.
@ManyToOne 속성 - (기본값)
- optional: false로 설정하면 연관된 엔티티가 항상 있어야한다. - (TRUE)
- fetch: 글로벌 페치 전략을 설정한다.
-
- 기본값
- @ManyToOne = FetchType.EAGER (엔티티를 로드할 때 연관관계에 있는 엔티티 모두 가져온다. )
- @OneToMany = FetchType.LAZY (getter() 로 접근할때 가져온다.)
- 기본값
- cascade: 영속성 전이 기능을 사용한다.
- targetEntity: 연관된 엔티티의 타입 정보를 설정한다. 거의 사용하지 않는 기능이다. 컬렉션을 사용해서 제네릭으로 타입 정보를 알 수 있다. 옛날 버전에서 썼다. 지금은 무의미한 기능
@OneToMany 속성
- mappedBy: 연관 관계의 주인 필드를 선택한다.
- fetch: 글로벌 페치 전략을 설정한다.
- 기본값
- @ManyToOne = FetchType.EAGER
- @OneToMany = FetchType.LAZY
- 기본값
- cascade: 영속성 전이 기능을 사용한다.
- targetEntity: 연관된 엔티티의 타입 정보를 설정한다. 거의 사용하지 않는 기능이다. 컬렉션을 사용해서 제네릭으로 타입 정보를 알 수 있다. 옛날 버전에서 썼다. 지금은 무의미한 기능
'Spring Framework > [인프런] Java ORM 표준 프로그래밍 - JPA' 카테고리의 다른 글
Chapter 08. 프록시와 연관 관계 관리 (0) | 2022.09.07 |
---|---|
Chapter 07. 고급 매핑 (2) | 2022.09.06 |
Chapter 05. 연관 관계 매핑 기초 (0) | 2022.09.05 |
Chapter 04. 엔티티 매핑 (entity mapping) (0) | 2022.09.04 |
Chapter 03. 영속성 관리 - 내부 동작 방식 (0) | 2022.09.03 |