JPA 006 - 상속 객체와 테이블 매핑
객체는 상속을 통해 공통된 필드를 부모클래스에 만들고, 나머지 필드를 각 자식클래스에 둘 수 있다.
하지만, 관계형 데이터베이스에는 상속의 개념이 없다. 대신 슈퍼타입-서브타입 모델링 기법을 사용할 수 있다.
ORM 은 객체의 상속과 데이터베이스의 슈퍼타입-서브타입 관계를 매핑한다.
JPA 에서는 객체와 테이블을 매핑할 때 3가지 전략을 제시한다.
1 Joined 전략
부모테이블과 자식테이블 각각을 모두 생성하고, 원하는 데이터를 조인해서 가져온다.
각 자식테이블은 부모와 같은 PK 를 가지면서, 이 PK는 부모의 PK와 관계를 맺는 FK 가 된다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Play {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
public class Game extends Play {
private int numberOfPlayers;
}
@Entity
public class Music extends Play{
private String artist;
}
@Entity
public class Video extends Play {
private String director;
}
테이블이 정규화가 되기 때문에 중복된 컬럼을 제거하여 저장공간을 효율적으로 사용할 수 있게 된다.
하지만 조회시 쿼리가 복잡할 수 있다. 테이블이 여러개 생기니 데이터베이스는 좀 더 복잡해질 것이다.
2 Single Table 전략
@Data
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "play_type")
public class Play {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
@Column(name = "play_type", insertable = false, updatable = false)
private String playType;
}
@Data
@ToString(callSuper = true)
@Entity
@DiscriminatorValue("MUSIC")
public class Music extends Play{
private String artist;
}
... Video, Game
Inheritance 의 기본 전략은 SINGLE_TABLE
이다. (추천 전략)
그리고 상속된 엔티티를 분류하기 위해 한 컬럼이 추가되는데 이 컬럼의 이름은 기본으로 DTYPE
이다. 이 컬럼의 이름을 @DiscriminatorColumn(name = "play_type")
와 같이 사용해 다른 이름으로 설정할수도 있다.
또한 DTYPE
값은 기본적으로 상속받은 객체의 클래스이름이 된다. 위의 경우는 Game, Music, Video 가 된다. 이 값 역시 @DiscriminatorValue("MUSIC")
와 같이 다른 이름으로 설정 수 있다.
조인이 필요없어서 쿼리가 단순한 장점이 있다.
단, 한 자식 엔티티가 저장했을 때, 다른 자식 엔티티의 컬럼은 모두 null 이 들어갈 것이다. ( Music 을 저장했을 때 Video, Game 의 컬럼은 null 일 것이다.)
아래와 같이 테스트를 해보자.
@DataJpaTest
class MusicTest {
@Autowired
MusicRepository musicRepository;
@Autowired
EntityManager em;
@Test
void Music_저장시_PlayType에_MUSIC이_들어간다() {
// given
Music music = new Music();
music.setName("사랑은 은하수 다방에서");
music.setPrice(600);
music.setArtist("10cm");
// when
Music save = musicRepository.save(music);
System.out.println(save);
// DataJpaTest 는 @Transactional 을 포함하고, 테스트 종료시 변경된 데이터를 rollback 한다.
// 따라서 insert 문이 생기지 않는다. 테스트에서는 insert 가 필요하므로 영속성컨텍스트를 초기화해준다.
em.flush();
em.clear();
Music foundMusic = musicRepository.findById(save.getId()).get();
System.out.println(foundMusic);
// then
assertThat(foundMusic.getPlayType()).isEqualTo("MUSIC");
}
}
테스트 출력
Music(super=Play(id=1, name=사랑은 은하수 다방에서, price=600, playType=null), artist=10cm)
Hibernate: insert into play (name, price, artist, play_type, id) values (?, ?, ?, 'MUSIC', ?)
Hibernate: select music0_.id as id2_7_0_, music0_.name as name3_7_0_, music0_.play_type as play_typ1_7_0_, music0_.price as price4_7_0_, music0_.artist as artist6_7_0_ from play music0_ where music0_.id=? and music0_.play_type='MUSIC'
Music(super=Play(id=1, name=사랑은 은하수 다방에서, price=600, playType=MUSIC), artist=10cm)
- 생성할 때 play_type 에 MUSIC 을 잘 넣는 것을 볼 수 있다.
- 조회할 때 where play_type = MUSIC 으로 잘 조회하는 것을 볼 수 있다.
3 Table per Class 전략
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Play {
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
public class Game extends Play {
private int numberOfPlayers;
}
@Entity
public class Music extends Play{
private String artist;
}
@Entity
public class Video extends Play {
private String director;
}
자식 테이블 3개가 만들어지고, 부모 테이블은 만들어지지 않는다.
부모 테이블의 컬럼이 자식 테이블에 각각 모두 들어간다. 정규화가 되지 않고 중복이 있는 구성이 된다.
여러 자식 테이블을 함께 조회할 때 성능이 느리고 (SELECT 여러 번에 UNION 을 해서 가져온다.) 자식 테이블을 통합해서 쿼리하기 힘들다.
그러므로 일반적으로 추천하지 않는 전략이다.
본 글은 자바 ORM 표준 JPA 프로그래밍 (김영한 저)참고하여 재구성했습니다.
'Java, Kotlin, Spring > JPA' 카테고리의 다른 글
JPA - 연관관계 매핑(N:1, 1:N, 1:1, N:N) (0) | 2021.09.21 |
---|---|
JPA - 단방향, 양방향 연관관계 매핑 (0) | 2021.09.20 |
Spring Data JPA - Entity, 관계 매핑 (0) | 2021.02.11 |
Spring Data JPA - Auditing (0) | 2021.02.11 |
Spring Data JPA - Transactional (0) | 2021.02.11 |
댓글