상속관계 매핑
관계형 데이터베이스에서는 상속 개념이 없다. 대신 슈퍼타입 서브타입 관계(Super-Type Sub-Type Relationship) 모델링 기법이 존재하고 객체의 상속 개념과 유사하다. ORM에서 상속 개념 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것을 의미한다.
관계형 데이터베이스설계는 물리모델과 논리 모델이 있다. 그림 도표처럼 논리 모델을 물리 모델로 구현해야 한다.
슈퍼타입, 서브타입 논리 모델을 실제 물리 모델을 구현하는 방법으로 3가지가 있다.
- 조인 전략 : 각각 테이블로 변환
- 단일 테이블 전략 : 통합 테이블로 변환
- 구현 클래스마다 테이블 전략 : 서브타입 테이블로 변환
상속 매핑은 부모 클래스에 @Inheritance 애노테이션을 사용해야 한다. 그리고 매핑 전략을 지정해야 한다.
매핑 전략을 지정할 때 @Inheritance 애노테이션의 strategy 속성을 통해 3가지 전략을 선택할 수 있다.
1. 조인 전략
@Inheritance(Strategy = InheritanceType.JOINED)
- 각각을 모두 테이블로 만들고 조회할 때 Join을 사용하는 방법이다.
- 조인 전략은 자식 엔티티, 부모 엔티티 각각을 모두 테이블로 만든다. 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 겸 외래키로 사용하는 전략이다.
- 따라서 조회할 때 조인 구문을 자주 사용한다.
- 여기서 주의할 점은 객체는 타입으로 구분할 수 있지만 테이블은 타입 개념이 없다. 따라서 타입을 구분하는 칼럼을 추가해야한다. 여기서 DTYPE 칼럼을 구분 칼럼으로 사용한다.
🔍 DTYPE
- 테이블의 타입을 구분하는 칼럼이다.
- @DiscriminatorColumn 애노테이션으로 테이블에 DTYPE을 삽입할 수 있다.
- 기본 값이 DTYPE이다.
- name 속성으로 구분 칼럼 명칭을 바꿀 수 있다.
- @DiscriminatorValue 애노테이션으로 DTYPE에 들어가는 값을 지정할 수 있다.
- @DiscriminatorValue 애노테이션을 안쓸 경우 기본 값으로 설정된다.
- 기본 값은 클래스 명이다.
📌 ItemV2 Entity
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
@Getter @Setter
public class ItemV2 {
@Id @GeneratedValue
@Column(name="item_id")
private Long id;
private String name;
private int price;
}
- @Inheritance 애노테이션의 strategy 속성을 InheritanceType.JOINED로 지정하여 조인 전략으로 설정했다.
- @DiscriminatorColumn 애노테이션을 통해 DTYPE 칼럼을 추가했다.
📌 Album Entity
@Entity
@Getter @Setter
@DiscriminatorValue("A")
public class Album extends ItemV2 {
private String artist;
}
- @DiscrimnatorValue 애노테이션을 통해 DTYPE에 들어갈 값을 'A'로 지정했다.
📌 Movie Entity
@Entity
@Getter @Setter
public class Movie extends ItemV2{
private String director;
private String actor;
}
- @DiscrimnatorValue 애노테이션을 생략함으로써 기본값인 클래스명(Movie)이 DTYPE 값으로 들어간다.
📌 Book Entity
@Entity
@DiscriminatorValue("B")
@Getter @Setter
public class Book extends ItemV2 {
private String author;
private String isbn;
}
📌 Main메서드
package hellojpa;
import hellojpa.domain2.Movie;
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 {
Movie movie = new Movie();
movie.setDirector("aaaa");
movie.setActor("bbbb");
movie.setName("바람과함께사라지다");
movie.setPrice(1000);
em.persist(movie);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 조인 전략을 설정하여 ITEMV2 / MOVIE / ALBUM / BOOK 의 테이블들이 각각 생겼다.
- ITEMV2 테이블에 DTYPE 값이 Movie인 데이터가 저장되었다.
- Movie 엔티티는 @DiscriminatorValue 애노테이션을 생략하여 기본 값인 클래스 명이 DTYPE 값으로 들어갔다.
- ITEMV2 테이블의 pk값인 ID값과 MOVIE 테이블의 pk값 겸 fk값인 ID값이 서로 일치해야 한다.
- 조인 전략에서 서브 타입 테이블들은 pk값 겸 fk 값을 슈퍼타입의 pk 값으로 사용하고 있다.
- 서브 타입 테이블의 pk 값 겸 fk 값의 컬럼명은 보통 기본값인 슈퍼 타입의 pk 값의 컬렴명으로 사용한다.
- 만약에 서브 타입 테이블의 pk값 겸 fk값의 칼럼 명을 변경하고 싶으면 @PrimaryKeyJoinColumn 애노테이션의 name 속성을 사용하면 된다.
package hellojpa.domain2;
import javax.persistence.Entity;
@Entity
@PrimaryKeyJoinColumn(name="MOVIE_ID") // ID 재정의
@Getter @Setter
public class Movie extends ItemV2{
private String director;
private String actor;
}
- @PrimaryKeyJoinColumn 애노테이션의 name 속성을 통해 서브 테이블인 MOVIE 테이블의 pk 값 겸 fk 값의 칼럼명을 name 속성 값인 MOVIE_ID로 바꿨다.
🔍 조인 전략 장점
- 테이블이 정규화된다.
- 외래 키 참조 무결성 제약조건을 활용할 수 있다.
- ex) 영화 가격을 알고 싶을 때 MOVIE 테이블을 조회하지 않고 ITEM 테이블만 조회 알수 있다.
- 저장공간을 효율적으로 사용한다.
🔍 조인 전략 단점
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
- 조회쿼리가 복잡하다.
- 데이터를 등록할 때 INSERT 구문을 두 번 실행한다.
// Movie 테이블을 등록하기 위해 insert 구분이 2번 실핼된다.
Hibernate:
/* insert hellojpa.domain2.Movie
*/ insert
into
ItemV2
(name, price, DTYPE, id)
values
(?, ?, 'Movie', ?)
Hibernate:
/* insert hellojpa.domain2.Movie
*/ insert
into
Movie
(actor, director, Movie_ID)
values
(?, ?, ?)
사실 조인 전략의 성능이 나쁘다는 것은 일반적인 단일 테이블 전략에 비해 상대적으로 안좋은 것 뿐이다. 조인 전략 성능 자체로 봤을 때 좋은 편이다. 그러므로 성능이 큰 단점은 아니다.
🧷 참고
- JPA 표준 명세는 구분 컬럼을 사용하도록 하지만, 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼(@DiscriminatorColumn) 없이도 동작한다.
2. 단일 테이블 전략
@Inheritance(Strategy = InheritanceType.SINGLE_TABLE)
- 논리모델을 하나의 슈퍼 타입 테이블로 합쳐버리고 DTYPE으로 구별하는 방법이다.
- 슈퍼 타입 테이블인 ITEM 테이블은 ALBUM / MOVIE / BOOK 칼럼들을 모두 가지고 있다.
- 슈퍼 타입 테이블 하나만 관리하면 된다.
- 단일 테이블 전략은 @DiscriminatorColumn 애노테이션을 적지 않아도 자동으로 적용되어 무조건 DTYPE이 존재한다.
- 자식 엔티티가 매핑한 칼럼은 모두 null 값 허용 (nullable) 해야한다.
📌 ItemV2 Entity
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
@Getter @Setter
public class ItemV2 {
@Id @GeneratedValue
@Column(name="item_id")
private Long id;
private String name;
private int price;
}
- @Inheritance 애노테이션의 strategy 속성을 InheritanceType.JOINED로 지정하여 조인 전략으로 설정했다.
- @DiscriminatorColumn 애노테이션을 통해 DTYPE 칼럼을 추가했다.
📌 Album Entity
@Entity
@Getter @Setter
@DiscriminatorValue("A")
public class Album extends ItemV2 {
private String artist;
}
- @DiscrimnatorValue 애노테이션을 통해 DTYPE에 들어갈 값을 'A'로 지정했다.
📌 Movie Entity
@Entity
@Getter @Setter
public class Movie extends ItemV2{
private String director;
private String actor;
}
- @DiscrimnatorValue 애노테이션을 생략함으로써 기본값인 클래스명(Movie)이 DTYPE 값으로 들어간다.
📌 Book Entity
@Entity
@DiscriminatorValue("B")
@Getter @Setter
public class Book extends ItemV2 {
private String author;
private String isbn;
}
📌 Main메서드
package hellojpa;
import hellojpa.domain2.Movie;
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 {
Movie movie = new Movie();
movie.setDirector("aaaa");
movie.setActor("bbbb");
movie.setName("바람과함께사라지다");
movie.setPrice(1000);
em.persist(movie);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- 단일 테이블 전략을 설정하여 오직 ITEMV2 테이블 하나만 생겼다.
- 단, ITEMV2 테이블은 서브타입 칼럼들을 모두 포함하고 있다.
- 그렇기 때문에 ITEMV2 칼럼들은 null 값이 들어올 수 있어야 한다.
🔍 단일 테이블 전략 장점
- 조인이 필요 없기에 일반적으로 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.
🔍 단일 테이블 전략 단점
- 자식 엔티티가 매핑한 칼럼은 모두 nullable 조건이 필요하다.
- 단일 테이블에 모든 것을 저장하기에 테이블이 엄청 커질 수 있다. 따라서 상황에 따라서 조회 성능이 느릴 수 있다.
- 이런 경우는 거의 없다.
3. 구현 클래스마다 테이블 전략
@Inheritance(Strategy = InheritanceType.TABLE_PER_CLASS)
- 결론 : 사용하지 않는다.
- 각각의 테이블마다 별개로 만들어서 따로 관리하는 방법이다.
- ITEM 테이블을 생성하지 않고 ALBUM / MOVIE / BOOK 테이블을 서브타입 슈퍼타입 관계가 아닌 각각 독립적인 테이블로 만든다.
- 구현 클래스마다 테이블 전략으로 설정하면 알아서 부모 클래스의 테이블이 생성되지 않는다.
- 이 때 슈퍼타입인 ITEM 테이블의 객체를 생성하지 않기 위해 ITEM 엔티티를 추상 클래스로 지정해야한다.
- 그렇기에 슈퍼타입의 테이블의 칼럼들(id, name, price)이 모두 서브 타입 테이블에 존재한다.
- 한마디로 슈퍼타입 서브타입 관계를 없애는 것이다.
- @DiscriminatorColumn 애노테이션을 사용할 수 없다.
- 서브 타입 슈퍼타입 관계가 아니기 때문에 굳이 DTYPE을 통해 구분할 이유가 없다.
🔍 구현 클래스마다 테이블 전략 장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적이다.
- not null 제약 조건을 사용할 수 있다.
🔍 구현 클래스마다 테이블 전략 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다.
- UNION SQL 필요
- 자식 테이블을 통합해서 쿼리하기가 힘들다.
4. 상속 개념 매핑시 사용되는 주요 애노테이션
- @Inheritance(strategy=InheritanceType.XXX)
- JOINED : 조인 전략
- SINGLE_TABLE : 단일 테이블 전략
- TABLE_PER_CLASS : 구현 클래스마다 테이블 전략
- @DiscriminatorColumn(name="DYTPE)
- @DiscriminatorValue("XXXX")
✔ 상속 관계 매핑 정리
- 기본적으로 조인 전략으로 선택한다.
- 정말 단순하면 단일 테이블로 할 것.
MappedSuperclass - 매핑 정보 상속
@MappedSupperclass
- 공통 매핑 정보가 필요할 때 사용한다.
- 부모 클래스는 테이블과 매핑하지 않고, 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공한다.
- 부모타입으로 조회, 검색이 불가능하다.
- 직접 생성자를 생성할 일이 없으므로 부모 객체를 추상 클래스로 만들 것을 권장한다.
- 실무에서 DB 설계할 때 공통 요소를 묶어서 설계하므로 @MappedSuperclass를 자주 사용한다.
- @MappedSupperclass 애노테이션이 붙은 클래스는 엔티티가 아니다. 그러므로 테이블과 매핑되지 않는다.
- @MappedSupperclass 애노테이션은 테이블과 관계가 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.
- 참고로 @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능하다.
📌 BaseEntity
package hellojpa.domain2;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
public class BasicEntity {
@Id @GeneratedValue
private Long id;
private String createBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}
📌 AAA Entity
package hellojpa.domain2;
import javax.persistence.Entity;
@Entity
public class AAA extends BasicEntity{
private String email;
}
📌 BBB Entity
package hellojpa.domain2;
import javax.persistence.Entity;
@Entity
public class BBB extends BasicEntity{
private String shopName;
}
- BasicEntity에 속해있는 매핑 정보들이 AAA 테이블과 BBB 테이블의 매핑정보로 들어가있다.
부모로부터 물려받은 매핑 정보를 수정하고 싶다면 @AttributeOrderride / @AttributeOrderrides 애노테이션을 사용하면 된다.
연관관계를 재정의하려면 @AssociationOverride / @AssociationOverrides 애노테이션을 사용하면 된다.
✔ @AttributeOrderride 사용
📌 BasicEntity
package hellojpa.domain2;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
public class BasicEntity {
@Id @GeneratedValue
private Long id;
private String createBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}
📌 AAA Entity
package hellojpa.domain2;
import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.Entity;
@Entity
@AttributeOverride(name ="id", column = @Column(name = "AAA_ID"))
public class AAA extends BasicEntity{
private String email;
}
- @AttributeOverride 애노테이션을 이용하여 부모로부터 물려 받은 속성인 'id'의 컬럼명을 'AAA_ID'로 수정했다.
- name 속성에는 부모로부터 물려 받은 속성 명(=컬럼명)을 적는다.
- column 속성에는 @Column 애노테이션의 name 속성을 사용하여 새롭게 수정할 속성 명을 적는다.
✔ @AttributeOrderrides 사용
- 둘 이상의 칼럼명을 바꾸고 싶다면 @AttributeOverrides 애노테이션을 사용하면 된다.
📌 BasicEntity
package hellojpa.domain2;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
public class BasicEntity {
@Id @GeneratedValue
private Long id;
private String createBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}
📌 AAA Entity
package hellojpa.domain2;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Entity;
@Entity
@AttributeOverrides({
@AttributeOverride(name ="id", column = @Column(name = "AAA_ID")),
@AttributeOverride(name ="lastModifiedDate", column = @Column(name = "AAA_date"))
})
public class AAA extends BasicEntity{
private String email;
}
💡 정리
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
public class BasicEntity {
private String createBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
}
- @MappedSuperclass로 등록자 / 등록일자 / 수정자 / 수정일자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.
- 또 위 코드의 공통 엔티티는 실무에서도 자주 쓰이는 속성들이다.
👀 참고자료
https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
자바 ORM 표준 JPA 프로그래밍 - 기본편 - 인프런 | 강의
JPA를 처음 접하거나, 실무에서 JPA를 사용하지만 기본 이론이 부족하신 분들이 JPA의 기본 이론을 탄탄하게 학습해서 초보자도 실무에서 자신있게 JPA를 사용할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
[JPA] 상속 관계 매핑 - 조인 전략
객체지향 언어에서 다루는 상속이라는 개념이 있지만, 관계형 데이터베이스에서는 상속이라는 개념이 없습...
blog.naver.com
https://catsbi.oopy.io/18aa1ae6-d001-4003-ae19-5bdc6144a12e#41004054-ebee-46e4-903b-56d018b82f03
고급 매핑
상속관계 매핑
catsbi.oopy.io
https://blog.naver.com/adamdoha/222139716154
[JPA] @MappedSuperclass
@MappedSuperclass 조인, 단일테이블, 구현클래스마다 테이블 전략은 상속 관계 매핑에서 사용되며, 부모...
blog.naver.com
'[JPA] > JPA 프로그래밍 - 기본편' 카테고리의 다른 글
[JPA] 값 타입 (0) | 2022.03.31 |
---|---|
[JPA] 프록시와 연관관계 관리 (0) | 2022.03.30 |
[JPA] 다양한 연관관계 매핑 (0) | 2022.03.29 |
[JPA] 연관관계 매핑 기초 (0) | 2022.03.28 |
[JPA] 엔티티 매핑 (0) | 2022.03.27 |