Skip to content

Latest commit

 

History

History
645 lines (519 loc) · 20.8 KB

08_advanced_mapping.md

File metadata and controls

645 lines (519 loc) · 20.8 KB

고급 매핑

목차

  • 상속관계 매핑

  • @MappedSuperclass

  • 실전 예제

상속관계 매핑

  • 객체는 상속관계가 존재하지만, 관계형 데이터베이스는 상속 관계가 없다.(대부분)
  • 그나마 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.
  • 상속관계 매핑이라는 것은 객체의 상속 구조와 DB의 슈퍼타입 서브타입 관계를 매핑하는 것이다.

슈퍼타입 서브타입 논리 모델 -> 물리모델 구현 방법

  • 객체는 상속을 지원하므로 모델링과 구현이 똑같지만, DB는 상속을 지원하지 않으므로 논리 모델을 물리 모델로 구현할 방법이 필요하다.

  • DB의 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법은 세가지 있다.

  • 중요한건, DB입장에서 세가지로 구현하지만 JPA에서는 어떤 방식을 선택하던 매핑이 가능하다.

  • JPA가 이 세가지 방식과 매핑하려면

    • **@Inheritance(strategy=InheritanceType.XXX)**의 stategy를 설정해주면 된다.
      • default 전략은 SINGLE_TABLE(단일 테이블 전략)이다.
      • InheritanceType 종류
        • JOINED
        • SINGLE_TABLE
        • TABLE_PER_CLASS
    • @DiscriminatorColumn(name="DTYPE")
      • 부모 클래스에 선언한다. 하위 클래스를 구분하는 용도의 컬럼이다. 관례는 default = DTYPE
    • @DiscriminatorValue("XXX")
      • 하위 클래스에 선언한다. 엔티티를 저장할 때 슈퍼타입의 구분 컬럼에 저장할 값을 지정한다.
      • 어노테이션을 선언하지 않을 경우 기본값으로 클래스 이름이 들어간다.
  • 객체의 상속관계 구현

    • Item

      @Entity
      @Inheritance(strategy = InheritanceType.XXX) // 상속 구현 전략 선택
      public class Item {
      
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
      
          private String name;
          private int price;
      }
    • Album

      @Entity
      public class Album extends Item {
      
          private String artist;
      }
    • Movie

      @Entity
      public class Movie extends Item {
      
          private String director;
          private String actor;
      }
    • Book

      @Entity
      public class Book extends Item {
      
          private String author;
          private String isbn;
      }

1 - 각각의 테이블로 변환하는 조인 전략(JOINED)

  • 가장 정규화 된 방법으로 구현하는 방식이다.

  • NAME, PRICE가 ITEM 테이블에만 저장되고, ALBUM, MOVIE, BOOK이 각자의 데이터만 저장한다.

  • Item 엔티티 - @Inheritance(strategy = InheritanceType.JOINED) 전략

    • 하이버네이트의 조인 전략에서는 @DiscriminatorColumn을 선언하지 않으면 DTYPE 컬럼이 생성되지 않는다.
    • 어차피 조인하면 앨범인지 무비인지 알 수 있다. 그래도, DTYPE을 넣어주는 것이 명확하다. 넣어주자.
    @Entity
    @Inheritance(strategy = InheritanceType.JOINED)
    @DiscriminatorColumn // 하위 테이블의 구분 컬럼 생성(default = DTYPE)
    public class Item {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        private String name;
        private int price;
    }
  • 실제 실행된 DDL

    • 테이블 4개 생성
    • 하위 테이블에 외래키 제약조건 생성. 하위 테이블 입장에서는 ITEM_ID가 PK이면서 FK로 잡아야 한다.
    • 조인 전략에 맞는 테이블들이 생섬됨.
    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 generated by default as identity,
            name varchar(255),
            price integer not null,
            primary key (id)
        )
    Hibernate: 
        create table Movie (
           actor varchar(255),
            director varchar(255),
            id bigint not null,
            primary key (id)
        )
        
        
    Hibernate: 
        alter table Album 
           add constraint FKcve1ph6vw9ihye8rbk26h5jm9 
           foreign key (id) 
           references Item
    Hibernate: 
        alter table Book 
           add constraint FKbwwc3a7ch631uyv1b5o9tvysi 
           foreign key (id) 
           references Item
    Hibernate: 
        alter table Movie 
           add constraint FK5sq6d5agrc34ithpdfs0umo9g 
           foreign key (id) 
           references Item
  • Movie 객체를 저장하면?

    • Insert 쿼리가 두개 나간다.
    • Item 테이블, Movie 테이블 저장.
    • DTYPE에 클래스 이름이 디폴트로 저장됨.
    Movie movie = new Movie();
    movie.setDirector("감독A");
    movie.setActor("배우B");
    movie.setName("분노의질주");
    movie.setPrice(35000);
    
    em.persist(movie);
    
    tx.commit();
    Hibernate: 
        /* insert advancedmapping.Movie
            */ insert 
            into
                Item
                (id, name, price, DTYPE) 
            values
                (null, ?, ?, 'Movie')
    Hibernate: 
        /* insert advancedmapping.Movie
            */ insert 
            into
                Movie
                (actor, director, id) 
            values
                (?, ?, ?)
  • Movie 객체를 조회하면?

    • flush(), clear() 해주면, DB에 insert쿼리 날리고, 1차 캐시 지우므로 find에서 SELECT 쿼리가 나간다.
    • Item과 inner join을 통해서 결과를 조회한다.
    Movie movie = new Movie();
    movie.setDirector("감독A");
    movie.setActor("배우B");
    movie.setName("분노의질주");
    movie.setPrice(35000);
    
    em.persist(movie);
    
    em.flush();
    em.clear();  //DB에 insert쿼리 날리고, 1차 캐시 지우므로 find에서 SELECT 쿼리가 나간다.
    
    em.find(Movie.class, movie.getId());
    
    tx.commit();
    Hibernate: 
        select
            movie0_.id as id2_2_0_,
            movie0_1_.name as name3_2_0_,
            movie0_1_.price as price4_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=?
    

2 - 통합 테이블로 변환하는 단일 테이블 전략(SINGLE_TABLE)

  • 서비스 규모가 크지 않고, 굳이 조인 전략을 선택해서 복잡하게 갈 필요가 없다고 판단 될 때에는

  • 한 테이블에 다 저장하고, DTYPE으로 구분하는 단일 테이블 전략을 선택할 수 있다.

  • INSERT 쿼리도 한 번, SELECT 쿼리도 한 번이다. 조인할 필요가 없고, 성능이 좋다.

  • 단일 테이블 적용

    • strategy를 SINGLE_TABLE로 변경하면 끝난다.

      • JPA의 장점이다. 테이블 구조의 변동이 일어났는데, 코드를 거의 손대지 않고 어노테이션만 수정했다.
      • 만약 JPA를 쓰지 않았더라면, 테이블 구조의 변경이 일어나면 거의 모든 코드를 손대야 할 것이다.
    • 단일 테이블 전략에서는 @DiscriminatorColumn을 선언해 주지 않아도, 기본으로 DTYPE 컬럼이 생성된다.

    • 한 테이블에 모든 컬럼을 저장하기 때문에, DTYPE 없이는 테이블을 판단할 수 없다.

      @Entity
      @DiscriminatorColumn
      @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
      public class Item {
      
          @Id
          @GeneratedValue(strategy = GenerationType.IDENTITY)
          private Long id;
      
          private String name;
          private int price;
      }
    • 실행된 DDL

      • 통합 테이블이 하나 생성된다.
      Hibernate: 
          create table Item (
             DTYPE varchar(31) not null,
              id bigint generated by default as identity,
              name varchar(255),
              price integer not null,
              artist varchar(255),
              author varchar(255),
              isbn varchar(255),
              actor varchar(255),
              director varchar(255),
              primary key (id)
          )
    • 조인 전략에서 실습했던 Movie 저장, 조회 예제를 그대로 돌려보면?

      • Item 테이블을 그냥 조회한다. 조인하지 않고, DTYPE을 검색 조건으로 추가해서 Movie를 조회

        Hibernate: 
            select
                movie0_.id as id2_0_0_,
                movie0_.name as name3_0_0_,
                movie0_.price as price4_0_0_,
                movie0_.actor as actor8_0_0_,
                movie0_.director as director9_0_0_ 
            from
                Item movie0_ 
            where
                movie0_.id=? 
                and movie0_.DTYPE='Movie'

3 - 서브타입 테이블로 변환하는 구현 클래스마다 테이블을 생성하는 전략(TABLE_PER_CLASS)

  • 조인 전략과 유사하지만, 슈퍼 타입의 컬럼들을 서브 타입으로 내린다. NAME, PRICE 컬럼들이 중복되도록 허용하는 전략이다.

  • 구현 클래스마다 테이블 생성 전략 적용

    • @Id 생성 전략 GenerationType.AUTO를 사용하는 경우 strategy를 TABLE_PER_CLASS로 변경하고

    • 이때, ITEM 엔티티는 실제 생성되는 테이블이 아니므로 abstract 클래스여야 하고, @DiscriminatorColumn도 필요가 없어진다.

      • 하지만, @Id 생성 전략 GenerationType.IDENTITY를 사용하는 경우 문제가 있다.
      • @Inheritance의 TABLE_PER_CLASS와 @Id의 GenerationType을 IDETITY를 같이 사용할 경우 에러 발생
    • Item 엔티티 설정

      @Entity
      @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
      public abstract class Item {
      
          @Id
          @GeneratedValue(strategy = GenerationType.AUTO)
          private Long id;
      
          private String name;
          private int price;
      }
    • 생성된 DDL

      • 하위 테이블 3개만 생성된다.

        Hibernate:
            create table Album (
               id bigint not null,
                name varchar(255),
                price integer not null,
                artist varchar(255),
                primary key (id)
            )
        Hibernate: 
            create table Book (
               id bigint not null,
                name varchar(255),
                price integer not null,
                author varchar(255),
                isbn varchar(255),
                primary key (id)
            )
        Hibernate: 
            create table Movie (
               id bigint not null,
                name varchar(255),
                price integer not null,
                actor varchar(255),
                director varchar(255),
                primary key (id)
            )
    • 조인 전략에서의 Movie 저장, 조회를 그대로 실행한 결과는?

      • Movie 테이블에 insert
      • Movie 테이블에서 select
      Hibernate: 
          /* insert advancedmapping.Movie
              */ insert 
              into
                  Movie
                  (name, price, actor, director, id) 
              values
                  (?, ?, ?, ?, ?)
      Hibernate: 
          select
              movie0_.id as id1_2_0_,
              movie0_.name as name2_2_0_,
              movie0_.price as price3_2_0_,
              movie0_.actor as actor1_3_0_,
              movie0_.director as director2_3_0_ 
          from
              Movie movie0_ 
          where
              movie0_.id=?
    • 문제점

      • 객체지향 프로그래밍에서는 MOVIE, ALBUM, BOOK 객체를 ITEM 타입으로도 조회할 수 있다.

        Movie movie = new Movie();
        movie.setDirector("감독A");
        movie.setActor("배우B");
        movie.setName("분노의질주");
        movie.setPrice(35000);
        
        em.persist(movie);
        
        em.flush();
        em.clear();
        
        em.find(Item.class, movie.getId());
        
        tx.commit();
      • 실행된 SQL

        • union all로 전체 하위 테이블을 다 찾는다.
        • INSERT 까진 심플했으나, 조회가 시작되면 굉장히 비효율적으로 동작한다.
        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_.author as author1_1_0_,
                item0_.isbn as isbn2_1_0_,
                item0_.actor as actor1_3_0_,
                item0_.director as director2_3_0_,
                item0_.clazz_ as clazz_0_ 
            from
                ( select
                    id,
                    name,
                    price,
                    artist,
                    null as author,
                    null as isbn,
                    null as actor,
                    null as director,
                    1 as clazz_ 
                from
                    Album 
                union
                all select
                    id,
                    name,
                    price,
                    null as artist,
                    author,
                    isbn,
                    null as actor,
                    null as director,
                    2 as clazz_ 
                from
                    Book 
                union
                all select
                    id,
                    name,
                    price,
                    null as artist,
                    null as author,
                    null as isbn,
                    actor,
                    director,
                    3 as clazz_ 
                from
                    Movie 
            ) item0_ 
        where
            item0_.id=?

상속관계 매핑 정리

  • 조인 전략

    • 장점
      • 테이블이 정규화가 되어있고,
      • 외래 키 참조 무결성 제약조건 활용 가능
        • ITEM의 PK가 ALBUM, MOVIE, BOOK의 PK이자 FK이다. 그래서 다른 테이블에서 아이템 테이블만 바라보도록 설계하는 것이 가능 하다
      • 저장공간 효율화
        • 테이블 정규화로 저장공간이 딱 필요한 만큼 소비된다.
    • 단점
      • 조회시 조인을 많이 사용한다. 단일 테이블 전략에 비하면 성능이 안나온다. 조인하니까.
      • 그래서 조회 쿼리가 복잡하다
      • 데이터 저장시에 INSERT 쿼리가 상위, 하위 테이블 두번 발생한다.
    • 정리
      • 성능 저하라고 되어있지만, 실제로는 영향이 크지 않다.
      • 오히려 저장공간이 효율화 되기 때문에 장점이 크다.
      • 기본적으로는 조인 정략이 정석이라고 보면 된다. 객체랑도 잘 맞고, 정규화도 되고, 그래서 설계가 깔끔하게 나온다.
  • 단일 테이블 전략

    • 장점
      • 조인이 필요 없으므로 일반적인 조회 성능이 빠르다.
      • 조회 쿼리가 단순핟.
    • 단점
      • 자식 엔티티가 매핑한 컬럼은 모두 NULL을 허용해야 한다.
      • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
      • 상황에 따라서 조인 전략보다 성능이 오히려 느려질 수 있다.
        • 보통 이 상황에 해당하는 임계점을 넘을 일은 많지 않다.
  • 구현 클래스마다 테이블 전략

    • 결론은
      • 이 전략은 쓰지말자.
      • ORM을 하다보면 데이터 쪽과 객체 쪽에서 trade off를 할 때가 있는데, 이 전략은 둘 다 추천하지 않는다.
    • 장점
      • 서브 타입을 명확하게 구분해서 처리할 때 효과적이다
      • NOT NULL 제약조건을 사용할 수 있다.
    • 단점
      • 여러 자식 테이블을 함께 조회할 때 성능이 느리다(UNION SQL)
      • 자식 테이블을 통합해서 쿼리하기 어렵다.
      • 변경이라는 관점으로 접근할 때 굉장히 좋지 않다.
        • 예를 들어, ITEM들을 모두 정산하는 코드가 있다고 가정할 때, ITEM 하위 클래스가 추가되면 정산 코드가 변경된다. 추가된 하위 클래스의 정산 결과를 추가하거나 해야 한다.
  • 상속관계 매핑 정리

    • 기본적으로는 조인 전략을 가져가자.
    • 그리고 조인 전략과 단일 테이블 전략의 trade off를 생각해서 전략을 선택하자.
    • 굉장히 심플하고 확장의 가능성도 적으면 단일 테이블 전략을 가져가자. 그러나 비즈니스 적으로 중요하고, 복잡하고, 확장될 확률이 높으면 조인 전략을 가져가자.

@MappedSuperclass

  • 객체의 입장에서 공통 매핑 정보가 필요할 때 사용한다.
  • id, name은 객체의 입장에서 볼 때 계속 나온다.
  • 이렇게 공통 매핑 정보가 필요할 때, 부모 클래스에 선언하고 속성만 상속 받아서 사용하고 싶을 때 @MappedSuperclass를 사용한다.
  • DB 테이블과는 상관없다. 아래에 보면 DB는 매핑 정보 다 따로 쓰고 있다. 객체의 입장이다.

코드로 이해하기

  • 생성자, 생성시간, 수정자, 수정시간을 모든 엔티티에 공통으로 가져가야 하는 상황에서

  • 아래와 같이 BaseEntity를 정의해서 활용할 수 있다.

  • BaseEntity.java

    • 매핑정보만 상속받는 Superclass라는 의미의 @MappedSuperclass 어노테이션 선언
    @Getter
    @Setter
    @MappedSuperclass
    public abstract class BaseEntity {
    
        private String createdBy;
    
        private LocalDateTime createdDate;
    
        private String lastModifiedBy;
    
        private LocalDateTime lastModifiedDate;
    }
  • Member.java, Team.java

    • BaseEntity 상속

      @Entity
      public class Member extends BaseEntity {
          ...
      }
      @Entity
      public class Team extends BaseEntity {
          ...
      }
  • 실행된 DDL

    • BaseEntity에 선언된 컬럼들이 생성 된다.

      Hibernate: 
          create table Member (
             id bigint generated by default as identity,
              createdBy varchar(255),
              createdDate timestamp,
              lastModifiedBy varchar(255),
              lastModifiedDate timestamp,
              age integer,
              description clob,
              roleType varchar(255),
              name varchar(255),
              locker_id bigint,
              team_id bigint,
              primary key (id)
          )
      Hibernate: 
          create table Team (
             id bigint generated by default as identity,
              createdBy varchar(255),
              createdDate timestamp,
              lastModifiedBy varchar(255),
              lastModifiedDate timestamp,
              name varchar(255),
              primary key (id)
          )
      ...

정리

  • 상속광계 매핑이 아니다.
  • @MappedSuperclass가 선언되어 있는 클래스는 엔티티가 아니다. 당연히 테이블과 매핑도 안된다.
  • 단순히 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공한다.
  • 조회, 검색이 불가하다. 부모 타입으로 조회하는 것이 불가능하다는 이야기.(em.find(BaseEntity) 불가능)
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만드는 것을 권장한다.
  • 테이블과 관계가 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할을 한다.
  • 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용한다.
  • 참고
    • JPA에서 @Entity 클래스는 @Entity나 @MappedSuperclass로 지정한 클래스만 상속할 수 있다.

Reference