자바 ORM 표준 JPA 프로그래밍 - 기본편 수업을 듣고 정리한 내용입니다.
🔔 고급 매핑
- 상속 관계 매핑 : 객체의 상속 관계를 데이터베이스에 어떻게 매핑하는지 다룬다.
@MappedSuperClass
: 등록일, 수정일 같이 여러 엔티티에서 공통으로 사용하는 매핑 정보만 상속받고 싶으면 이 기능을 사용하면 된다.- 복합 키와 식별 관계 매핑 : 데이터베이스의 식별자가 하나 이상일 때 매핑하는 방법을 다룬다. 그리고 데이터베이스 설계에서 이야기하는 식별 관계와 비식별 관계에 대해서도 다룬다.
- 조인 테이블 : 테이블은 외래 키 하나로 연관관계를 맺을 수 있지만 연관관계를 관리하는 연결 테이블을 두는 방법도 있다. 여기서는 이 연결 테이블을 매핑 하는 방법을 다룬다.
- 엔티티 하나에 여러 테이블 매핑하기 : 보통 엔티티 하나에 테이블 하나를 매핑하지만 엔티티 하나에 여러 테이블을 매핑하는 방법도 있다. 여기서는 이 매핑 방법을 다룬다.
- 관계형 데이터베에스에는 상속이라는 개념이 없다.
- 대신 슈퍼 타입, 서브 타입 관계(
Super-Type Sub-Type Relationship
)라는 모델링 기법이 객체의 상속 개념과 가장 유사하다!ORM
에서 상속 관계 매핑 : 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.
슈퍼타입 서브타입 논리 모델 → 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택할 수 있다.
JOINED TABLE
과 같이 각각을 모두 테이블로 만들고 조회할 때 조인을 사용한다. JPA에서는 조인 전략이라고 한다.SINGLE TABLE
과 같이 테이블을 하나만 사용해서 통합한다. JPA에서는 단일 테이블 전략이라고 한다.CONCRETE TABLE
과 같이 서브 타입마다 하나의 테이블을 만든다. JPA에서는 구현 클래스마다 테이블 전략이라 한다.
Jpa 기본 전략 자체가 단일 테이블로 매핑한다.
조인 전략(Joined Strategy) : 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서
기본 키 + 외래 키
로 사용하는 전략
✔️ 조인 전략 사용
조회할 때 조인을 자주 사용한다.
Item
import javax.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
//@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name; // 이름
private int price; // 가격
// getter, setter
}
Book
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;
@Entity
public class Book extends Item {
private String author;
private String isbn;
// getter, setter
}
Movie
@Entity
public class Movie extends Item {
private String director; // 감독
private String actor; // 배우
// getter, setter
}
Album
@Entity
//@DiscriminatorValue("A")
public class Album extends Item{
private String artist;
// getter, setter
}
JpaMain
try {
Movie movie = new Movie();
movie.setDirector("add");
movie.setActor("bbb");
movie.setName("바람과 함꼐 살아지다.");
movie.setPrice(10000);
em.persist(movie);
tx.commit();
}
실행 결과
Item
, Movie
가 생성된 것을 확인할 수 있다.
JpaMain
에 추가
em.flush();
em.clear();
Movie findMovie = em.find(Movie.class, movie.getId());
System.out.println("findMovie = " + findMovie);
실행시
(내부 조인 : 내부 조인은 둘 이상의 테이블에 존재하는 공통 속성의 값이 같은 것을 결과로 추출한다.)
위에서는 DTYPE
이 생략되었는데 이제 DTYPE
을 추가한다.
✔️ DTYPE을 사용시 좋은점이
넣을시,
현재 ITEM이 부모이고, ALBUM, BOOK, MOVIE가 자식인 상태이다.
DiscriminatorColumn
에 name
을 생략할시 DTYPE
이 자동 추가된다.
주의할 점
DTYPE
컬럼을 구분 컬럼으로 사용한다.@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatiorColumn(name = "DTYPE")
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name; // 이름
private int price; // 가격
...
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director; // 감독
private String actor; // 배우
...
}
@Inheritance(strategy = InheritanceType.JOINED)
: 상속 매핑은 부모 클래스에 @Inheritance
를 사용해야 한다. 그리고 매핑 전략을 지정해야 하는데 여기서는 조인 전략을 사용하므로 InheritanceType.JOINED
를 사용했다.@DiscriminatorColumn(name = "DTYPE")
: 부모 클래스에 구분 컬럼을 지정했다. 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다. (기본 값이 DTYPE
이므로 @DiscriminatorColumn
으로 줄여서 사용해도 된다.)@DiscriminatorValue("M")
: 엔티티를 저장할 때 구분 칼럼에 입력할 값을 지정한다. 만약 영화 엔티티를 저장하면 구분 칼럼인 DTYPE
에 값 M
이 저장된다.
기본 값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용한다.
만약, 자식 테이블의 기본 키 컬럼명을 변경하고 싶을 때는 @PrimaryKeyJoinColumn
을 사용하면 된다.
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID") // ID 재정의
public class Book extends Item {
private String author; // 작가
private String isbn; // ISBN
...
}
BOOK
테이블의 ITEM_ID
기본 키 컬럼명을 BOOK_ID
로 변경하였다.
📌 조인 전략 정리
(1) 장점
- 테이블이 정규화된다.
- 외래 키 참조 무결성 제약조건을 활용할 수 있다.
- 저장공간을 효율적으로 사용한다.
(2) 단점
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
- 조회 쿼리가 복잡하다.
- 데이터를 등록할 때
INSERT SQL
을 두 번 실행한다.(3) 특징
JPA
표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼(@DiscriminatorColumn
) 없이도 동작한다.(4) 관련 애노테이션
@PrimaryKeyJoinColumn
,@DiscriminatorColumn
,@DiscriminatorValue
- 단일 테이블 전략(
Single-Table Strategy
) 이름 그대로 테이블을 하나만 사용한다.- 구분 컬럼(DRIVE)으로 어떤 자식 데이터가 저장되었는지 구분한다.
- 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다!
⚠️ 주의점
자식 엔티티가 매핑한 컬럼은 모두null
을 허용해야 한다.
ex)Book
엔티티를 저장하면ITEM
테이블의AUTHOR
,ISBN
컬럼만 사용하고 다른 엔티티와 매핑된ARTIST
,DIRECTOR
,ACTOR
컬럼은 사용하지 않으므로null
이 입력되기 때문이다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name; // 이름
private int price; // 가격
...
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item { ... }
@Entity
@DiscriminatorValue("M")
public class Movie extends Item { ... }
@Entity
@DiscriminatorValue("B")
public class Book extends Item { ... }
Inheritance.SINGLE_TABLE
: 단일 테이블 전략을 사용한다. 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 한다.
실행 결과
📌 단일 테이블 전략 정리
(1) 장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.
(2) 단점
- 자식 엔티티가 매핑한 컬럼은 모두
null
을 허용해야 한다.- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 상황에 따라서는 조회 성능이 느려질 수 있다.
(3) 특징
- 구분 칼럼을 꼭 사용해야 한다. 따라서
@DiscriminatorColumn
을 꼭 설정해야 한다.@DiscriminatorValue
를 지정하지 않으면 기본적으로 엔티티 이름을 사용한다. ex)Movie
,Albumn
,Book
- 구현 클래스마다 테이블 전략(
Table-per-Concrete-Class Strategy
)은 자식 엔티티마다 테이블을 만든다.- 자식 테이블 각각에 필요한 컬럼이 모두 있다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name; // 이름
private int price; // 가격
...
}
@Entity
public class Album extends Item { ... }
@Entity
public class Movie extends Item { ... }
@Entity
public class Book extends Item { ... }
InheritanceType.TABLE_PER_CLASS
: 구현 클래스마다 테이블 전략을 사용한다. 이 전략은 자식 엔티티마다 테이블을 만든다. (일반적으로 추천하지 않는 전략이다.)
📌 구현 클래스마다 테이블 전략 정리
(1) 장점
- 서브 타입을 구분해서 처리할 때 효과적이다.
not null
제약조건을 사용할 수 있다.(2) 단점
- 여러 자식 테이블 함께 조회할 때 성능이 느리다. (
SQL
에UNION
을 사용해야 한다.)- 자식 테이블을 통합해서 쿼리하기 어렵다.
(3) 특징
- 구분 컬럼을 사용하지 않는다.
💡 참고
- 조인 전략 : 기본으로 사용, 비즈니스 복잡할 때 사용한다.
- 단일 테이블 전략 : 가장 단순할 때 사용한다.
- 구현 클래스마다 테이블 전략 : 거의 사용하지 않는다.
- 지금까지, 상속 관계 매핑은 부모 클래스와 자식 클래스를 모두 데이터베이스 테이블과 매핑했다.
- 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶으면
@MappedSuperclass
를 사용하면 된다.
✔️ @MappedSuperclass
@Entity
는 실제 테이블과 매핑되지만, @MappedSuperclass
는 실제 테이블과 매핑되지는 않는다.객체
테이블
현재 회원(Member
)과 판매자(Seller
)는 서로 관계가 없는 테이블과 엔티티다.
테이블은 그대로 두고 객체 모델의 id
, name
두 공통 속성을 부모 클래스로 모으고 객체 상속 관계로 만들어보자!
@MappedSuperclass
public class BaseEntity {
private String createdBy;
private LocalDateTime createDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
...
}
@Entity
public class Member extends BaseEntity {
// createBy, createDate, lastModifiedBy, lastModifiedDate 상속
...
}
@Entity
public class Team extends BaseEntity{
// createBy, createDate, lastModifiedBy, lastModifiedDate 상속
...
}
extends BaseEntity
를 추가
JpaMain
try {
Member member = new Member();
member.setCreateDate(LocalDateTime.now());
member.setCreatedBy("lee");
member.setUsername("user");
em.persist(member);
em.flush();
em.clear();
tx.commit();
}
실행 결과
BaseEntity
에는 객체들이 주로 사용하는 공통 매핑 정보를 정의했다!BaseEntity
의 매핑 정보를 물려받았다.BaseEntity
는 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하면 된다. → @MappedSuperclass
📌 @MappedSuperclass 정리
- 상속관계 매핑과 관련이 없다.
- 엔티티가 아니며, 테이블과 매핑이 아니다.
- 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공한다.
- 부모 타입으로 조회, 검색 불가 (
em.find(BaseEntity)
불가)- 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장한다.
- 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.
- 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.
💡 참고
@Entity
클래스는 엔티티나@MappedSuperclass
로 지정한 클래스만 상속 가능하다.
📣 요구사항 추가
- 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장할 수 있다.
- 모든 데이터는 등록일과 수정일이 필수이다.
✔️ 도메인 모델
✔️ 테이블 설계
Album
package jpabook.jpashop.domain;
import javax.persistence.Entity;
@Entity
public class Album extends Item{
private String artist;
private String etc;
public String getArtist() {
return artist;
}
public void setArtist(String artist) {
this.artist = artist;
}
public String getEtc() {
return etc;
}
public void setEtc(String etc) {
this.etc = etc;
}
}
Book
package jpabook.jpashop.domain;
import javax.persistence.Entity;
@Entity
public class Book extends Item{
private String author;
private String isbn;
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getIsbn() {
return isbn;
}
public void setIsbn(String isbn) {
this.isbn = isbn;
}
}
Movie
package jpabook.jpashop.domain;
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;
}
}
Item
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public abstract class Item extends BaseEntity {
...
}
Item
클래스를 추상 클래스로 만들고 상속, 단일 테이블 속성을 지정해서 실행할시
Item
테이블이 생성된다.
try {
Book book = new Book();
book.setName("JPA");
book.setAuthor("김영한");
em.persist(book);
tx.commit();
}
데이터 추가까지 잘된다.
✔️ 등록일과 수정일 필드 추가
BaseEntity
package jpabook.jpashop.domain;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@MappedSuperclass
public class BaseEntity {
private String createdBy;
private LocalDateTime createDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public LocalDateTime getCreateDate() {
return createDate;
}
public void setCreateDate(LocalDateTime createDate) {
this.createDate = createDate;
}
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;
}
}
클래스 생성 후, 모든 클래스에서 BaseEntity
를 상속받도록 추가한다.
등록일, 수정일이 성공적으로 추가된다.
참고 : https://velog.io/@jsj3282/20.-%EA%B3%A0%EA%B8%89-%EB%A7%A4%ED%95%911