공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
상속 관계 매핑
- 관계형 데이터베이스는 상속 관계가 없다.
- 하지만 슈퍼타입, 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.
- 상속관계 매핑 : 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑한다.
슈퍼타입 서브타입 논리모델을 실제 물리 모델로 구현하는 방법은 크게 3가지가 있다.
1. 조인전략(각각 테이블로 변환)
Item
@Entity
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
Album, Movie, Book
@Entity
public class Album extends Item{
private String artist;
}
================================
@Entity
public class Movie extends Item {
private String director;
private String actor;
}
================================
@Entity
public class Book extends Item{
private String author;
private String isbn;
}
- JPA의 기본 전략은 한 테이블에 다 만들어지도록 되어있다.
부모 테이블에서 @Inheritance로 전략을 선택할 수 있다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
}
- 조인 방식으로 테이블이 만들어진 것을 확인할 수 있다.
이제 데이터를 넣어보자.
public class JpaMain {
//생략
Movie movie = new Movie();
movie.setDirector("AAA");
movie.setActor("BBB");
movie.setName("바람과 함께 사라지다.");
movie.setPrice(10000);
em.persist(movie);
}
- ID는 같은 값이다. ITEM에서 PK가 MOVIE 테이블에서 PK이면서 FK이다.
쿼리 실행 과정을 확인하기 위해 flush()와 clear()를 사용하여 1차 캐시를 비운 후, 다시 findMovie를 호출해 보자.
Movie movie = new Movie();
movie.setDirector("AAA");
movie.setActor("BBB");
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);
JPA가 상속관계인 경우나 조인이 필요할 때는 조인을 해서 값을 가져오는 것을 확인할 수 있다.
ITEM 테이블에 Dtype을 빼고 생성했는데 이번에는 넣어서 생성해 보자.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public class Item {}
@DiscriminatorColumn를 적어주면 Dtype이 생기고 엔티티명이 입력된다.
@DiscriminatorColumn(name = "")으로 원하는 이름으로 변경도 가능하다.
자식 엔티티의 이름을 변경하고 싶으면 각각의 엔티티에 @DiscriminatorValue("")으로 이름을 변경할 수 있다.
@Entity
@DiscriminatorValue("A")
public class Album extends Item{}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {}
@Entity
@DiscriminatorValue("B")
public class Book extends Item{}
Album은 A, Movie는 M, Book은 B라고 지정하면 위와 같이 지정한 이름으로 값이 입력된다.
이는 DB에서 조회했을 때 어떤 자식의 데이터인지 구분하기 용이하다.
장점
- 테이블 정규화가 되어있다.
- 외래 키 참조 무결성 제약조건 활용가능하다.
- 저장공간이 효율적이다.
단점
- 조회 시 조인을 많이 사용해서 성능 저하가 있다.
- 조회 쿼리가 복잡하다.
- 데이터 저장할 때 INSERT SQL을 2번 호출한다.
2. 단일 테이블 전략(통합 테이블로 변환)
DB를 설계했는데 테이블을 싱글로 하는 게 더 유리한 경우에 사용한다.
한 테이블에 모든 컬럼을 입력하고 DTYPE으로 구분한다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public class Item {}
전략을 SINGLE_TABLE로 변경하면 된다.
- INSERT도 한 번에 하고 조인할 필요도 없기 때문에 성능상 이점이 있다.
- @DiscriminatorValue을 생략해도 자동으로 생성된다.
조인테이블과 싱글테이블로 DB 설계를 바꿨는데 코드는 바뀐 게 없고 애노테이션의 전략부분만 수정되었다.
이것이 JPA의 장점 중 하나이다. (쿼리를 수정할 필요가 없다.)
장점
- 조인이 필요가 없으므로 일반적으로 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.
단점
- 자식 엔티티가 매핑한 컬럼은 모두 NULL을 허용한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
- 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
3. 구현 클래스마다 테이블 전략(서브타입 테이블로 변환) - 쓰면 안 되는 전략
부모 테이블을 생성하지 않고 자식테이블에서 모두 관리하는 전략이다.
전략은 TABLE_PER_CLASS로 수정한다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn
public abstract class Item {}
- 부모 테이블(ITEM)에 있던 PRICE와 NAME을 자식 테이블인 MOVIE에서 관리한다.
이 전략은 데이터를 삽입할 때는 괜찮은데 조회할 때는 좋지 않다.
만약 ITEM을 조회한다고 하면 3개의 자식 테이블을 모두 UNION ALL로 조회한다.
장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적이다.
- NOT NULL 제약조건을 사용할 수 있다.
단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다(UNION SQL 필요)
- 자식 테이블을 통합해서 쿼리 하기 어렵다.
기본적으로는 조인 전략을 쓰고 테이블을 설계했는데 단순할 때, 단일 테이블 전략을 쓰자.
Mapped Superclass - 매핑 정보 상속
등록일자, 수정일자, 등록자, 수정자 등 모든 테이블에 필요한 컬럼이 중복으로 있을 때 사용한다.
공통된 속성을 BaseEntity로 만들고 @MappedSuperclass로 자식들에게 매핑 정보만 제공할 수 있다.
@MappedSuperclass
public class BaseEntity {
private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}
@Entity
public class Team extends BaseEntity{}
=========================================
@Entity
public class Member extends BaseEntity{}
public class JpaMain {
public static void main(String[] args) {
//생략
try {
Member member = new Member();
member.setUsername("user1");
member.setCreatedBy("kim");
member.setCreatedDate(LocalDateTime.now());
em.persist(member);
em.flush();
em.clear();
tx.commit();
}
}
- 속성 정보만 상속받아서 테이블 생성을 한다.
- BaseEntity는 엔티티가 아니다. 오로지 속성만 제공한다.
정리
- @MappedSuperclass는 상속관계 매핑을 지원하지 않는다.
- 이 어노테이션이 붙은 클래스는 엔티티도 아니고 테이블과 직접 매핑되지도 않는다.
- 단지, 부모 클래스를 상속받는 자식 클래스에 매핑 정보를 제공할 뿐이다.
- 따라서 @MappedSuperclass로 지정된 클래스는 직접 조회하거나 검색할 수 없다 (em.find(BaseEntity) 불가).
- 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만드는 것이 좋다.
- 이 클래스는 테이블과 관계없이 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.
- 주로 등록일, 수정일, 등록자, 수정자 같은 공통 정보를 모을 때 사용된다.
- JPA에서 extends로 상속받을 때는 @Entity나 @MappedSuperclass로 지정한 클래스만 상속할 수 있다.
실전 예제 4 - 상속관계 매핑
요구사항 추가
- 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다.
- 모든 데이터는 등록일과 수정일이 필수다.
도메인 모델
- 기존 도메인에 Album, Book, Movie를 추가하여 상속관계를 만든다.
- 테이블은 싱글 테이블로 설계한다.
Item
package jpabook.jpashop.domain;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract 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<>();
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;
}
}
Album, Book, Movie
@Entity
public class Album extends Item{
private String artist;
private String etc;
}
=====================================
@Entity
public class Book extends Item{
private String author;
private String isbn;
}
=====================================
@Entity
public class Movie extends Item{
private String director;
private String actor;
}
- 싱글 테이블로 모든 속성이 포함되어 생성된다.
Book으로 데이터를 저장해 보자.
public class JpaMain {
public static void main(String[] args) {
//생략
try {
Book book = new Book();
book.setName("JPA");
book.setAuthor("김영한");
em.persist(book);
tx.commit();
}
}
다음으로는 등록일과 수정일을 공통으로 매핑시켜 줄 BaseEntity를 만든다.
@MappedSuperclass
public abstract class BaseEntity {
private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public LocalDateTime getCreatedDate() {
return createdDate;
}
public void setCreatedDate(LocalDateTime createdDate) {
this.createdDate = createdDate;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public void setLastModifiedBy(String lastModifiedBy) {
this.lastModifiedBy = lastModifiedBy;
}
public LocalDateTime getLastModifiedDate() {
return lastModifiedDate;
}
public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
this.lastModifiedDate = lastModifiedDate;
}
}
기존에 만들었던 엔티티에 extends BaseEntity로 상속시킨다.
(Album은 Item을 상속받고 Item은 BaseEntity를 상속받으니 안 해도 된다.)
모든 테이블에 등록일, 등록자, 수정일, 수정자가 생성된다.
싱글 테이블이기 때문에 ITEM에 모든 컬럼이 포함되어 있다. 따로 만들고 싶으면 ITEM의 @Inheritance 전략을 JOINED로 바꾸면 된다.
바꿔보자
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract class Item extends BaseEntity{
//생략
}
자식 테이블이 다 따로 생성되는 것을 확인할 수 있다.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 영속성 전이와 고아 객체 (0) | 2024.07.16 |
---|---|
[JPA] 프록시와 연관관계 관리 (0) | 2024.07.16 |
[JPA] 다양한 연관관계 매핑 (0) | 2024.07.14 |
[JPA] 연관관계 매핑 기초 (2) | 2024.07.14 |
[JPA] 엔티티 매핑 (0) | 2024.07.12 |