DEV/JPA

[JPA] 고급매핑 - 상속관계 매핑

Imvory 2024. 4. 5. 00:07

* 정보전달의 목적이 아닌 개인 스터디 정리 글 입니다.

 

강의 : 인프런 <자바 ORM 표준 JPA 프로그래밍 기본편>

교육자 : 김영한


상속관계 매핑

: 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑

- 관계형 데이터베이스는 상속관계가 없다.

- 슈퍼타입 서브타입 관계의 모델링 기법이 객체 상속과 유사하다.

 

* DB 논리모델을 물리적으로 구현할수있는 방법

- 조인전략 : 모든 클래스를 각각의 테이블로 변환

- 단일 테이블 전략 : 통합된 테이블 한개로 변환

- 구현 클래스마다 테이블 전략 : 서브타입 테이블로 변환 (슈퍼타입 클래스 제외)

 

* 주요 어노테이션

  • @Inheritance(strategy=InheritanceType.XXX)
    • JOINED : 조인 전략
    • SINGLE_TABLE : 단일 테이블 전략
    • TABLE_PER_CLASS : 구현 클래스마다 테이블 전략
  • @DiscriminatorColumn : 하위 엔티티명이 DTYPE 필드로 들어옴 (name 속성 바꿀수있음)
  • @DiscriminatorValue("XXX") : DTYPE을 엔티티명이 아닌 다른 값으로 바꿀수있음

 

조건 ) 슈퍼타입 ITEM 클래스와 서브타입 ALBUM, MOVIE, BOOK 클래스가 존재 할때


* 조인전략
상위테이블과 하위테이블로 나누어 저장하는 방법
상위 엔티티를 상속받음
- 상위, 하위 테이블 각각 insert 됨
- 데이터를 가져올땐 상위,하위테이블을 조인해서 가져옴

 

장점

- 테이블 정규화

- 외래키 참조 무결성 제약조건 활용가능

- 저장공간 효율화

 

단점

- 조회시 조인을 많이사용 -> 성능 저하

- 조회 쿼리가 복잡

- 데이터 저장시 INSERT SQL 2번 호출

 

상위 추상클래스 Item

import jakarta.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn
public abstract 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;
    }
}


하위 서브클래스 Movie, Album, Book

import jakarta.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;
    }
}

@Entity
public class Album extends Item {

    private String artist;
}

@Entity
public class Book extends Item {

    private String author;
    private String isbn;
}

 

 

상속관계를 JOIN으로 설정 후 Movie 객체에 값을 저장 및 출력

import jakarta.persistence.*;

import java.util.List;

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("장재현");
            movie.setName("파묘");
            movie.setActor("최민식");
            movie.setPrice(12000);

            em.persist(movie);

            em.flush();
            em.clear();

            Movie findMovie = em.find(Movie.class, movie.getId());

            System.out.println("findMovie ==> " + findMovie);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

결과1 ) 테이블이 모두 각각 생성됨

 

결과2 ) Item, Movie 각각 insert 쿼리가 두번 나감

 

결과3 ) 값을 가져올때 Join해서 가져옴

 

결과 4 ) DB가 각각 생성된 모습

 


* 단일테이블전략

- 모든 클래스를 하나의 클래스에 통합하여 저장하는 방법
- @DiscriminatorColumn 사용 안해도 DTYPE이 자동으로 생성

 

장점

- 조인이 필요 없어 일반적으로 조회 성능이 빠름

- 조회 쿼리가 단순

 

단점

- 자식 엔티티가 매핑한 컬럼은 모두 NULL이 허용됨

- 단일 테이블에 모든것을 저장하므로 테이블이 커질 수 있다.

상황에 따라 조회 성능이 오히려 느려질 수 있다. -> 임계점이 넘어야하는데 극히 드물긴함

 

 

나머지는 그대로 두고,

Item 클래스에서 상속관계 매핑 타입을 SIGLE_TABLE로 변경, @DiscriminatorColumn 제거해도 상관X

import jakarta.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Item {

    @Id @GeneratedValue
    private long id;
    private String name;
    private int price;

  	...
}

 

결과1 ) 테이블 하나만 생성되고 모든 필드가 들어감

 

결과2 ) 한번만 insert됨

 

결과3 ) select도 조인없이 한번에 가능

 

 

* 구현 클래스마다 테이블 전략

- 슈퍼타입 클래스를 제외한 서브타입 클래스가 각각 테이블로 저장
- @DiscriminatorColumn 어노테이션 필요없음 (각각 테이블로 관리하기 때문)
- 이 전략은 데이터베이스 설계자와 ORM 전문가 둘다 추천하지 않는 방식

 

장점

- 서브 타입을 명확하게 구분해서 처리할 때 효과적

- NOT NULL 제약조건 사용 가능

 

단점

- 여러 자식 테이블을 함께 조회할 때 성능이 느림 (UNION SQL 필요)

- 자식 테이블을 통합해서 쿼리하기 어려움

 

마찬가지로 상속관계 매핑 타입만 TABLE_PER_CLASS로 변경

import jakarta.persistence.*;

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {

    @Id @GeneratedValue
    private long id;
    private String name;
    private int price;

  	...
}

 

id로 Movie객체가 아닌 Item객체 가져오기

import jakarta.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("장재현");
            movie.setName("파묘");
            movie.setActor("최민식");
            movie.setPrice(12000);

            em.persist(movie);

            em.flush();
            em.clear();

            Item findItem = em.find(Item.class, movie.getId());
            System.out.println("findItem ==> " + findItem);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

 

결과1 ) Item 테이블을 제외한 Movie, Book, Album 테이블만 생성

 

결과2 ) 1개의 테이블에만 insert

 

결과3 ) 상위 타입의 값을 가져오려고 테이블을 union하는 모습 (생략됨)

 


@MappedSuperclass
: 공통 매핑 정보가 필요할 때 사용 (id, name, date ...)

- 상속관계 매핑이 아님

- 엔티티가 아님, 따라서 테이블과 매핑하는것이 아님

- 부모클래스를 상속받는 자식클래스에 매핑 정보만 제공

- 조회, 검색 불가 (em.find(BaseEntity) 불가)

- 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만들것을 권장

- 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할

참고 : @Entity클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능

 

공통적으로 사용할 생성자, 생성일, 수정자, 수정일 정보를 모은 BaseEntity

import jakarta.persistence.MappedSuperclass;

import java.time.LocalDateTime;

@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;
    }
}

 

Member, Team 클래스 상속받기

@Entity
public class Member extends BaseEntity {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    @Column(name = "USERNAME")
    private String name;
    
    ...
}

@Entity
public class Team extends BaseEntity {
    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;
    private String name;
    
    ...
}

 

 

결과 ) Member, Team 테이블에 필드가 각각 들어가는 모습 확인