반응형
5. 다대다 [N : N]
- 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음.
- 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함
- 객체는 컬렉션을 사용해서 객체 2개로 다대다 관계 가능
사용방법
- @ManyToMany 사용
- @JoinTable로 연결 테이블 지정
- 다대다 매핑: 단방향, 양방향 가능
다대다 매핑의 한계
- 편리해보이지만 실무에서 사용X
- 연결 테이블이 단순히 연결만 하고 끝나지 않음
- 주문시간, 수량 같은 데이터가 들어올 수 있음
다대다 한계 극복
- 연결 테이블용 엔티티 추가(연결 테이블을 엔티티로 승격)
- @ManyToMany → @OneToMany, @ManyToOne
6. 실전 예제 - 3. 다양한 연관관계 매핑
배송, 카테고리 추가 - 엔티티
- 주문과 배송은 1:1(@OneToOne)
- 상품과 카테고리는 N:M(@ManyToMany)
배송, 카테고리 추가 - ERD
배송, 카테고리 추가 - 엔티티 상세
N:M 관계는 1:N, N:1로 변경
- 테이블의 N:M 관계는 중간 테이블을 이용해서 1:N, N:1로 변경한다.
- 실전에서는 중간테이블이 단순하지 않다.
- @ManyToMany는 제약: 필드 추가 X, 엔티티 테이블 불일치
- 실전에서는 @ManyToMany 사용 X
@JoinColumn
- 외래 키를 매핑할 때 사용
- 속성
- name: 매핑할 외래 키 이름. 기본값 → 필드명 + _ + 참조하는 테이블의 기본 키 컬럼명
- referencedColumnName: 외래 키가 참조하는 대상 테이블의 컬럼명. 기본값 → 참조하는 테이블의 기본키 컬럼명
- foreignKey(DDL): 외래 키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용
- unique / nullable / insertable / updatable / columnDefinition / table: @Column의 속성과 같음
@ManyToOne
- 다대일 관계 매핑
- 속성
- optional: false로 설정하면 연관된 엔티티가 항상 있어야 한다. 기본값 → TRUE
- fetch: 글로벌 페치 전략을 설정.
[기본값]- @ManyToOne=FetchType.EAGER
- @OneToMany=FetchType.LAZY
- cascade: 영속성 전이 기능 사용
- targetEntity: 연관된 엔티티의 타입 정보를 설정. 이 기능은 거의 사용 X. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있음
@OneToMany
- 일대다 관계 매핑
- 속성
- mappedBy: 연관관계의 주인 필드를 선택
- fetch: 글로벌 페치 전략을 설정
[기본값]- @ManyToOne=FetchType.EAGER
- @OneToMany=FetchType.LAZY
- cascade: 영속성 전이 기능 사용
- targetEntity: 연관된 엔티티의 타입 정보를 설정. 이 기능은 거의 사용 X. 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있음
// Member
package jpabook.jpashop.domain;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
private String name;
private String city;
private String street;
private String zipcode;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>(); // 관례상 new ArrayList<>(); 선언
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;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
}
// Order
package jpabook.jpashop.domain;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@Table(name = "ORDERS") // DB는 ORDER가 예약어로 걸려있는 경우가 존재하기에 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;
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
private LocalDateTime orderDate; // order_date(스프링부트 기본설정 캐멀을 언더스코어로 변경해줌)
@Enumerated(EnumType.STRING)
private OrderStatus status;
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Member getMember() {
return member;
}
public void setMember(Member member) {
this.member = member;
}
public LocalDateTime getOrderDate() {
return orderDate;
}
public void setOrderDate(LocalDateTime orderDate) {
this.orderDate = orderDate;
}
public OrderStatus getStatus() {
return status;
}
public void setStatus(OrderStatus status) {
this.status = status;
}
}
// Delivery
package jpabook.jpashop.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
@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;
}
// Category
package jpabook.jpashop.domain;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@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"))
private List<Item> items = new ArrayList<>();
}
// Item
package jpabook.jpashop.domain;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@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> categories = new ArrayList<>();
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;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getStockQuantity() {
return stockQuantity;
}
public void setStockQuantity(int stockQuantity) {
this.stockQuantity = stockQuantity;
}
}
// OrderItem
package jpabook.jpashop.domain;
import javax.persistence.*;
@Entity
public class OrderItem {
@Id
@GeneratedValue
@Column(name = "ORDER_ITEM_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "ORDER_ID")
private Order order;
@ManyToOne
@JoinColumn(name = "ITEM_ID")
private Item item;
private int orderPrice;
private int count;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Order getOrder() {
return order;
}
public void setOrder(Order order) {
this.order = order;
}
public Item getItem() {
return item;
}
public void setItem(Item item) {
this.item = item;
}
public int getOrderPrice() {
return orderPrice;
}
public void setOrderPrice(int orderPrice) {
this.orderPrice = orderPrice;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
1. 상속관계 매핑
- 객체는 상속관계가 있지만 관계형 데이터베이스는 상속 관계 X
- 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속관계와 유사
- 상속관계 매핑: 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것
주요 어노테이션
@Inheritance(strategy=InheritanceType.XXX)
- JOINED: 조인 전략
- SINGLE_TABLE: 단일 테이블 전략
- TABLE_PER_CLASS: 구현 클래스마다 테이블 전략
@DiscriminatorColumn(name=”DTYPE”)
- DTYPE(기본값)을 넣는 것이 관례. 원하면 변경 가능
@DiscriminatorValue(”XXX”)
- 자식 클래스에서 예를 들어 Album, Movie, Book 대신 A,M,B를 넣고 싶을 때 자식 클래스에 어노테이션을 달고 XXX에 각각 값을 대입한다. default는 엔티티 이름이다.
em.flush()&em.clear()후 em.find()를 실행 시, 기존의 테이블이 제거x, select문이 나가지 않는 오류
https://www.inflearn.com/questions/17219/기존-테이블이-삭제되지-않는-문제
위 링크처럼 해결할 수 있고, 나는 H2 데이터베이스 1.4.200을 쓰고 있었기에 에러가 발생하였다. 따라서 1.4.199를 새로 설치하고 pom.xml 설정을 변경함으로써 문제를 해결하였다.
조인 전략(정석)
- JpaMain
try {
Movie movie = new Movie();
movie.setDirector("aaaa");
movie.setActor("bbbb");
movie.setName("바람과 함께 사라지다");
movie.setPrice(10000);
em.persist(movie);
em.flush();
em.clear();
Movie findMovie = em.find(Movie.class, movie.getId());
System.out.println("findMovie = " + findMovie);
tx.commit();
}
- Item
package hellojpa;
import javax.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
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;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
- Album
package hellojpa;
import javax.persistence.Entity;
@Entity
public class Album extends Item {
private String artist;
}
- Movie
package hellojpa;
import javax.persistence.Entity;
@Entity
public class Movie extends Item {
private String director;
private String actor;
public String getDirector() {
return director;
}
public void setDirector(String director) {
this.director = director;
}
public String getActor() {
return actor;
}
public void setActor(String actor) {
this.actor = actor;
}
}
- Book
package hellojpa;
import javax.persistence.Entity;
@Entity
public class Book extends Item {
private String author;
private String isbn;
}
- 출력
Hibernate:
call next value for hibernate_sequence
Hibernate:
/* insert hellojpa.Movie
*/ insert
into
Item
(name, price, DTYPE, id)
values
(?, ?, 'Movie', ?)
Hibernate:
/* insert hellojpa.Movie
*/ insert
into
Movie
(actor, director, id)
values
(?, ?, ?)
Hibernate:
select
movie0_.id as id2_2_0_,
movie0_1_.name as name3_2_0_,
movie0_1_.price as price4_2_0_,
movie0_.actor as actor1_6_0_,
movie0_.director as director2_6_0_
from
Movie movie0_
inner join
Item movie0_1_
on movie0_.id=movie0_1_.id
where
movie0_.id=?
findMovie = hellojpa.Movie@2e1792e7
조인전략 정리
- 장점
- 테이블 정규화
- 외래 키 참조 무결성 제약조건 활용가능
- 저장공간 효율화
- 단점
- 조회시 조인을 많이 사용, 성능 저하
- 조회 쿼리가 복잡함
- 데이터 저장시 INSERT SQL 2번 호출
단일 테이블 전략
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
// @DiscriminatorColumn을 쓰지 않아도 DTYPE이 자동으로 생성된다.
Hibernate:
create table MemberProduct (
id bigint not null,
count integer not null,
orderDateTime timestamp,
price integer not null,
MEMBER_ID bigint,
PRODUCT_ID bigint,
primary key (id)
)
단일 테이블 전략 정리
- 장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
- 조회 쿼리가 단순함
- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용(데이터 무결성이 애매하다)
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있음. 상황에 따라서 조회 성능이 오히려 느려질 수 있음.
구현 클래스마다 테이블 전략(사용하지 말 것)
조상인 Item 클래스를 abstract 클래스로 만들고, 전략을 TABLE_PER_CLASS로 변경한다.
→ ITEM 테이블은 생성 X
데이터를 넣을 때는 상관없지만 조회할 때 상당히 느리다.
Item item = em.find(Item.class, movie.getId());
System.out.println("item = " + item);
Hibernate:
select
item0_.id as id1_2_0_,
item0_.name as name2_2_0_,
item0_.price as price3_2_0_,
item0_.artist as artist1_0_0_,
item0_.author as author1_1_0_,
item0_.isbn as isbn2_1_0_,
item0_.actor as actor1_6_0_,
item0_.director as director2_6_0_,
item0_.clazz_ as clazz_0_
from
( select
id,
name,
price,
artist,
null as author,
null as isbn,
null as actor,
null as director,
1 as clazz_
from
Album
union
all select
id,
name,
price,
null as artist,
author,
isbn,
null as actor,
null as director,
2 as clazz_
from
Book
union
all select
id,
name,
price,
null as artist,
null as author,
null as isbn,
actor,
director,
3 as clazz_
from
Movie
) item0_
where
item0_.id=?
item = hellojpa.Movie@c68a5f8
단순한 조회쿼리에 이정도의 SQL 구문이 실행된다.
→ 이 전략은 데이터베이스 설계자와 ORM 전문가 둘다 추천 X
구현 클래스마다 테이블 전략 정리
- 장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적
- not null 제약조건 사용 가능
- 단점
- 성능 느리다(UNION SQL 필요)
- 자식 테이블을 통합해서 쿼리하기 어려움
2. @MappedSuperclass
- 공통 매핑 정보가 필요할 때 사용(id, name)
- BaseEntity
@MappedSuperclass
public class BaseEntity {
@Column(name = "INSERT_MEMBER")
private String createdBy;
private LocalDateTime createdDate;
@Column(name = "UPDATE_MEMBER")
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
- Member
public class Member extends BaseEntity
- Team
public class Team extends BaseEntity
- 결과
Hibernate:
create table Member (
MEMBER_ID bigint not null,
INSERT_MEMBER varchar(255),
createdDate timestamp,
UPDATE_MEMBER varchar(255),
lastModifiedDate timestamp,
USERNAME varchar(255),
LOCKER_ID bigint,
TEAM_ID bigint,
primary key (MEMBER_ID)
)
...
Hibernate:
create table Team (
TEAM_ID bigint not null,
INSERT_MEMBER varchar(255),
createdDate timestamp,
UPDATE_MEMBER varchar(255),
lastModifiedDate timestamp,
name varchar(255),
primary key (TEAM_ID)
)
- 특징
- 상속관계 매핑을 하지 않는다.
- 엔티티(@Entity)가 아니다 → JPA에서 관리하지 X, 테이블과 매핑하지 않는다.
- 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공한다.
- 조회 및 검색이 불가하다(em.find(BaseEntity) 불가능)
- 직접 생성해서 사용할 일이 없으므로 추상 클래스를 권장한다.
- 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.
- 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.
- 참고: @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속이 가능하다.
3. 실전 예제 - 4. 상속관계 매핑
요구사항 추가
- 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다.
- 모든 데이터는 등록일과 수정일이 필수다.
도메인 모델
SINGLE_TABLE
- JpaMain
try {
Book book = new Book();
book.setName("JPA");
book.setAuthor("김영한");
em.persist(book);
tx.commit();
}
- Item
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item
- 결과
Hibernate:
create table Item (
DTYPE varchar(31) not null,
ITEM_ID bigint not null,
name varchar(255),
price integer not null,
stockQuantity integer not null,
actor varchar(255),
director varchar(255),
author varchar(255),
isbn varchar(255),
artist varchar(255),
etc varchar(255),
primary key (ITEM_ID)
)
JOINED 전략 결과
BaseEntity 넣고 난 후
Item, Member, Delivery, Order, … 모두 BaseEntity를 상속받음
반응형
'TIL' 카테고리의 다른 글
[TIL-9 / 230627] 프록시 - 즉시로딩과 지연로딩, 고아 객체 (0) | 2023.06.27 |
---|---|
[TIL-8 / 230623] JPA 프록시 (0) | 2023.06.24 |
[TIL-6 / 230621] JPA 다양한 연관관계 매핑 (0) | 2023.06.21 |
[TIL-5 / 230620] JPA 연관관계 매핑 기초, 연관관계의 주인 (0) | 2023.06.20 |
[TIL-4 / 230618] Java - 인터페이스 (0) | 2023.06.19 |