ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JPA] 고급매핑 - 상속관계 매핑
    DEV/JPA 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 테이블에 필드가 각각 들어가는 모습 확인

    댓글

Designed by Tistory.