[JPA] 상속관계 매핑
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
이 글은 인프런에서 제공하는 자바 ORM 표준 JPA 프로그래밍 - 기본편 강의를 참고했고
강의 내용을 다시 복습하면서 정리하려는 목적으로 작성합니다.
객체지향 언어에서는 명확하게 상속 관계가 존재합니다.
부모의 속성에서 더 작은 그룹으로 분리해서 객체를 관리해야할 때 우리는 상속을 사용합니다.
데이터 베이스에서도 부모의 속성에서 더 작은 그룹으로 분리해서 관리해야 할 수도 있습니다.
하지만, 관계형 데이터베이스는 상속 관계가 존재하지 않습니다.
이러한 문제점을 해결하기 위해 슈퍼타입, 서브타입 관계라는 모델링 기법을 사용했습니다.
ORM
을 사용하여 우리는 객체의 상속 구조와 DB
의 슈퍼타입 서브타입 관계를 매핑 할 수 있습니다.
상속관계를 매핑하기 위해서는 먼저 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현해야 합니다.
슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법은 3가지가 있습니다.
- 각각 테이블로 변환
- 조인 전략
- 통합 테이블로 변환
- 단일 테이블 전략
- 서브타입 테이블로 변환
- 구현 클래스마다 테이블 전략
주요 애노테이션
JPA
에서는 다음과 같이 애노테이션을 사용해서 테이블 전략을 선택할 수 있습니다.
@Inheritance(strategy=InheritanceType.XXX)
- JOINED
- 조인 전략
- SINGLE_TABLE
- 단일 테이블 전략
- TABLE_PER_CLASS
- 구현 클래스마다 테이블 전략
@DiscriminatorColumn(name=“DTYPE”)
DTYPE
생성
@Entity
@Getter @Setter
@DiscriminatorColumn(name = "DTYPE")
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private Integer price;
}
@DiscriminatorValue(“XXX”)
자식 테이블의 DTYPE
을 XXX
로 변경Book
을 저장하면 Item
테이블 DTYPE
에 B
가 저장됩니다.(기본값은 엔티티 이름(Book
) 저장)
@Entity
@Getter @Setter
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
}
조인 전략
예를 들어서 ALBUM
데이터를 추가하면
INSERT INTO ITEM … ;
INSERT INTO ALBUM … ;
이렇게 SQL
이 2개 발생합니다.
조회는 PK
, FK
가 ITEM_ID
이기 때문에 ITEM_ID
로 조인하면 됩니다.
SELECT * FROM ITEM i
JOIN ALBUM a ON a.ITEM_ID = i.ITEM_ID;
DTYPE
으로 ALBUM
, MOVIE
, BOOK
을 구분할 수 있습니다.
아래 예제에서 확인해 봅시다.
Item
조인 전략을 사용하여 Entity
를 매핑하는 코드는 아래와 같습니다.
(@Inheritance(strategy = InheritanceType.JOINED)
을 작성하여 조인전략을 사용합니다.)
@Entity
@Getter @Setter
// 조인 전략
@Inheritance(strategy = InheritanceType.JOINED)
// name은 기본값이 DTYPE
@DiscriminatorColumn(name = "DTYPE")
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private Integer price;
}
Album
@Entity
@Getter @Setter
public class Album extends Item {
private String artist;
}
Book
@Entity
@Getter @Setter
public class Book extends Item {
private String author;
private String isbn;
}
Movie
@Entity
@Getter @Setter
public class Movie extends Item {
private String director;
private String actor;
}
Hibernate:
create table Album (
artist varchar(255),
id bigint not null,
primary key (id)
)
Hibernate:
create table Book (
author varchar(255),
isbn varchar(255),
id bigint not null,
primary key (id)
)
Hibernate:
create table Item (
DTYPE varchar(31) not null,
id bigint not null,
name varchar(255),
price integer,
primary key (id)
)
Hibernate:
create table Movie (
actor varchar(255),
director varchar(255),
id bigint not null,
primary key (id)
)
Item
, Album
, Book
, Movie
테이블이 생성된것을 확인할 수 있습니다.
Movie
를 저장해 볼까요?
Movie movie = new Movie();
movie.setDirector("봉준호");
movie.setActor("송강호");
movie.setName("기생충");
movie.setPrice(15000);
em.persist(movie);
Hibernate:
/* insert com.study.purejpa.entity.item.Movie
*/ insert
into
Item
(name, price, DTYPE, id)
values
(?, ?, 'Movie', ?)
Hibernate:
/* insert com.study.purejpa.entity.item.Movie
*/ insert
into
Movie
(actor, director, id)
values
(?, ?, ?)
데이터 저장시 INSERT SQL
2번 호출된것을 확인할 수 있습니다!
DTYPE
에 Movie
라고 저장된것을 확인할 수 있습니다.
조회 쿼리는 어떻게 발생하는지 확인해 볼까요?
em.find(Movie.class, 1L);
Hibernate:
select
movie0_.id as id1_2_0_,
movie0_1_.name as name2_2_0_,
movie0_1_.price as price3_2_0_,
movie0_.actor as actor1_3_0_,
movie0_.director as director2_3_0_
from
Movie movie0_
inner join
Item movie0_1_
on movie0_.id=movie0_1_.id
where
movie0_.id=?
inner join
을 사용해서 조회하는 것을 알수 있습니다.
장점
- 테이블 정규화
- 외래 키 참조 무결성 제약조건 활용가능
- 저장공간 효율화
단점
- 조회시 조인을 많이 사용
- 성능 저하
- 조회 쿼리가 복잡함
- 데이터 저장시
INSERT SQL
2번 호출- 엄청난 단점은 아니라고 봅니다. 🤗
단일 테이블 전략
단순하고 확장 가능성이 없어보일때 사용하는 것을 추천 드립니다.
논리 모델을 하나의 테이블로 합치는 방법 입니다.DTYPE
으로 ALBUM
, MOVIE
, BOOK
을 구분할 수 있습니다.
Item@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
으로 설정하면 단일 테이블 전략을 사용할 수 있습니다.
(사실 @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
를 생략해도 됩니다.!)
@Entity
@Getter @Setter
// 단일 테이블 전략
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private Integer price;
}
create table Item (
DTYPE varchar(31) not null,
id bigint not null,
name varchar(255),
price integer,
artist varchar(255),
actor varchar(255),
director varchar(255),
author varchar(255),
isbn varchar(255),
primary key (id)
)
테이블을 조회하면 다음과 같은 결과를 보여줍니다.
장점
- 조인이 필요 없으므로 일반적인 조회 성능이 빠름 💨
- 조회 쿼리가 단순
단점
- 자식 엔티티가 매핑한 컬럼은 모두
null
허용(치명적 단점!) - 단일 테이블에 모든 것을 저장하기 때문에 테이블 사이즈가 커질 수 있음
- 상황에 따라서 조회 성능이 느려질 수 있음
- 하지만 이런 상황은 거의 존재하지 않음
구현 클래스마다 테이블 전략
이 전략은 사용하지 않는것이 좋습니다.😵💫
ALBUM
, MOVIE
, BOOK
을 독립적인 테이블로 만드는 방법 입니다.
(각 테이블 마다 기존의 Item
테이블이 담당했던 name
, price
를 중복으로 갖게 됩니다.)
데이터베이스 설계자와 ORM
전문가 둘 다 추천 하지 않습니다.
ItemItem
을 추상(abstract
) 클래스로 만들고,@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
으로 설정하면 구현 클래스마다 테이블 전략을 사용할 수 있습니다.
@DiscriminatorColumn(name = "DTYPE”)
을 넣어도 사용할 수 없습니다.Item
테이블을 사용하지 않고 ALBUM
, MOVIE
, BOOK
테이블을 독립적으로 사용하기 때문입니다.^^
@Entity
@Getter @Setter
// 구현 클래스마다 테이블 전략
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private Integer price;
}
Hibernate:
create table Album (
id bigint not null,
name varchar(255),
price integer,
artist varchar(255),
primary key (id)
)
Hibernate:
create table Book (
id bigint not null,
name varchar(255),
price integer,
author varchar(255),
isbn varchar(255),
primary key (id)
)
Hibernate:
create table Movie (
id bigint not null,
name varchar(255),
price integer,
actor varchar(255),
director varchar(255),
primary key (id)
)
Item
테이블이 생성되지 않고 ALBUM
, MOVIE
, BOOK
테이블만 생성되는 것을 확인할 수 있습니다.^^
데이터를 저장하면 다음과 같은 결과를 확인할 수 있습니다.
Movie movie = new Movie();
movie.setDirector("봉준호");
movie.setActor("송강호");
movie.setName("기생충");
movie.setPrice(15000);
em.persist(movie);
오… 엄청 좋아보이는데요…?
그런데 부모타입으로 조회를 하면 어떻게 될까요? 🤔
Item item = em.find(Item.class, 1L);
System.out.println("Item="+item);
Movie
의 부모는 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_.actor as actor1_3_0_,
item0_.director as director2_3_0_,
item0_.author as author1_1_0_,
item0_.isbn as isbn2_1_0_,
item0_.clazz_ as clazz_0_
from
( select
id,
name,
price,
artist,
null as actor,
null as director,
null as author,
null as isbn,
1 as clazz_
from
Album
union
all select
id,
name,
price,
null as artist,
actor,
director,
null as author,
null as isbn,
2 as clazz_
from
Movie
union
all select
id,
name,
price,
null as artist,
null as actor,
null as director,
author,
isbn,
3 as clazz_
from
Book
) item0_
where
item0_.id=?
부모타입(Item
)으로 조회하게 되면 자식 클래스를 모두 조회할수 밖에 없습니다.
장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적
not null
제약조건 사용 가능
단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느림(
UNION ALL
필요)UNION ALL
은 여러 쿼리문들이 합쳐서 하나의 쿼리문으로 만들어주는 방법 입니다.- 중복된 값을 모두 조회합니다.
UNION
은UNION ALL
과 동일하지만, 중복된 값을 제거하고 조회합니다.
- 자식 테이블을 통합해서 쿼리하기 어려움
정리
- 조인 전략
- 부모 테이블과 각각 자식 테이블로 변환(슈퍼타입과 서브타입을 조인하여 조회)
- 일반적으로 사용하는 것을 추천
- 단일 테이블 전략
- 통합 테이블로 변환(하나의 테이블에서 관리)
- 비지니스가 매우 단순하고 확장 가능성이 없을때 사용하는 것을 추천
- 구현 클래스마다 테이블 전략
- 서브 타입 테이블로 변환(슈퍼타입을 없애고, 서브타입으로만 사용)
- 비추천
'0+ 스프링 > 0 + 스프링 ORM(JPA)' 카테고리의 다른 글
[JPA] 하이버네이트 프록시와 지연로딩(Lazy), 즉시로딩(Eager) (0) | 2023.02.22 |
---|---|
[JPA] @MappedSuperclass(공통 매핑 정보 해결 + 스프링 적용) (0) | 2023.02.20 |
[JPA] 연관관계 매핑(@ManyToOne, @OneToMany, @OneToOne, @ManyToMany) (0) | 2023.02.20 |
[JPA] 연관관계 매핑 개념(패러다임 불일치 해결) (0) | 2023.02.18 |
[JPA] 엔티티 매핑(@Entity, @Table) (0) | 2023.02.16 |