[JPA] 고급 매핑
Updated:
들어가며
해당 글은 자바 ORM 표준 프로그래밍을 정리한 글입니다.
상속 관계 매핑, @MappedSuperclass
, 복합키와 식별관계 매핑에 대해서 알아본다.
상속 관계 매핑
조인 전략
- 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없기에 type을 구분할 수 있는 컬럼(default = “DTYPE”)을 사용해야 한다.
@Inheritance(strategy = InheritanceType.JOINED)
@Entity
@DiscriminatorColumn(name = "DTYPE")
public 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 {
private String artist;
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
}
create table item (
dtype varchar(31) not null,
item_id bigint not null,
name varchar(255),
price integer not null,
primary key (item_id)
)
create table album (
artist varchar(255),
item_id bigint not null,
primary key (item_id)
)
create table movie (
actor varchar(255),
director varchar(255),
item_id bigint not null,
primary key (item_id)
)
@Inheritance(strategy = InheritanceType.JOINED)
- 상속 매핑은 부모 클래스에 @Inheritance를 사용 해야 한다.
- 매핑 전략을 지정해야 하는데 조인 전략은 JOINED를 사용한다.
@DiscriminatorColumn(name = “DTYPE”)
- 부모 클래스에 구분 컬럼을 지정한다.
- 이 컬럼으로 저장된 자식 테이블을 구분할 수 있다.
- 기본값이 DTYPE이다.
@DiscriminatorValue(“M”)
- 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.
- 기본값은 엔티티 이름으로 사용한다.
장점
- 테이블이 정규화된다.
- 외래 키 참조 무결성 제약조건을 활용할 수 있다.
- 저장공간을 효율적으로 사용한다.
단점
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
- 조회 쿼리가 복잡하다
- 데이터를 등록할 INSERT SQL을 두 번 실행한다.
@Test
void insertTest() {
Movie movie = new Movie();
em.persistAndFlush(movie);
em.clear();
Movie result = em.find(Movie.class, movie.getId());
System.out.println(result);
}
insert
into
item
(name, price, dtype, item_id)
values
(?, ?, 'M', ?)
insert
into
movie
(actor, director, item_id)
values
(?, ?, ?)
select
movie0_.item_id as item_id2_1_0_,
movie0_1_.name as name3_1_0_,
movie0_1_.price as price4_1_0_,
movie0_.actor as actor1_5_0_,
movie0_.director as director2_5_0_
from
movie movie0_
inner join
item movie0_1_
on movie0_.item_id=movie0_1_.item_id
where
movie0_.item_id=?
단일 테이블 전략
- InheritanceType.SINGLE_TABLE을 사용한다.
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
@Data
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 해당 부분만 변경
@Entity
@DiscriminatorColumn(name = "DTYPE")
public class Item {
@Id @GeneratedValue
@Column(name = "item_id")
private Long id;
private String name;
private int price;
}
create table item (
dtype varchar(31) not null,
item_id bigint not null,
name varchar(255),
price integer not null,
artist varchar(255),
actor varchar(255),
director varchar(255),
primary key (item_id)
)
장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다.
- 조회 쿼리가 단순하다.
- 추가적인 테이블 생성이 없다.
단점
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있어, 상황에 따라 조회 성능이 오히려 느려질 수 있다.
insert
into
item
(name, price, actor, director, dtype, item_id)
values
(?, ?, ?, ?, 'M', ?)
select
movie0_.item_id as item_id2_0_0_,
movie0_.name as name3_0_0_,
movie0_.price as price4_0_0_,
movie0_.actor as actor6_0_0_,
movie0_.director as director7_0_0_
from
item movie0_
where
movie0_.item_id=?
and movie0_.dtype='M'
구현 클래스마다 테이블 전략
- 추천하지 않는 전략
장점
- 서브 타입을 구분해서 처리할 때 효과적이다.
- not null 제약조건을 사용할 수 있다.
단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느리다.(SQL에 UNION을 사용해야 한다.)
- 자식 테이블을 통합해서 쿼리하기 어렵다.
@MappedSuperclass
- 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용
- @AttributeOverrides, @AttributeOverride를 사용하여 부모로부터 물려받은 매핑 정보를 재정의 가능
- 테이블에 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
- em.find()나 JPQL에서 사용할 수 없다.
- 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장
@MappedSuperclass
public abstract class AbstractEntity {
@Id
private Long id;
private OffsetDateTime createdTime;
private OffsetDateTime modifiedTime;
private OffsetDateTime removedTime;
}
@Entity
public class Book extends AbstractEntity {
}
create table book (
id bigint not null,
created_time timestamp,
modified_time timestamp,
removed_time timestamp,
primary key (id)
)
- 부모로 부터 상속 받은 값을 변경하기 위해서는 @AttributeOverrides를 이용해서 변경할 수 있다.
@Entity
@AttributeOverrides(value = {@AttributeOverride(name = "id", column = @Column(name = "book_id"))})
public class Book extends AbstractEntity {
// Id와 time 상
}
create table book (
book_id bigint not null, // id에서 book_id로 변
created_time timestamp,
modified_time timestamp,
removed_time timestamp,
primary key (book_id)
)
복합 키와 식별 관계 매핑
식별관계
- 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계
- 자식 테이블로 내려갈 수록 기본 키 컬럼이 늘어나 조인시 복잡해지고 기본키 인덱스가 불필요하게 커질 수 있음
- 자연 키 컬럼들로 만들게 되면 나중에 비지니스 요구사항이 변경됨에 따라 영향을 줄 수 있음
- 식별 관계의 경우 @GenerateValue를 사용하지 못 한다.
비식별 관계
- 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용
- 필수적 비식별 관계
- 외래 키에 NULL을 허용하지 않는다. 연관관계를 필수적으로 맺어야 한다.
- 선택적 비식별 관계
- 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.
@IdClass
- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
- Parent.id1과 ParentId.id1
- Serializable 인터페이스를 구현해야 한다.
- equals, hashCode를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
private String id1;
@Id
private String id2;
}
public class ParentId implements Serializable {
private Long id1;
private Long id2;
}
create table parent (
id1 varchar(255) not null,
id2 varchar(255) not null,
primary key (id1, id2)
)
@EmbeddedId
- @Embeddable 어노테이션을 붙여주어야 한다.
- Serializable 인터페이스를 구현해야 한다.
- equals, hashCode를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
- 식별 관계에서 사용하는 경우 @MapsId를 사용해야 한다.
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
}
@Embeddable
public class ParentId implements Serializable {
private Long id1;
private Long id2;
}
create table parent (
id1 varchar(255) not null,
id2 varchar(255) not null,
primary key (id1, id2)
)
마치며
상속 관계일 때 매핑을 하는 방법에 대해서 알아보았다.
실무에서도 상속관계일 때 JOINED
와 SINGLE_TABLE
중 에 무엇을 쓸지는 상황에 따라 다르겠지만
복잡도를 고려해볼 때는 SINGLE_TABLE
이 낮아 적용하기에는 쉬워 보인다고 생각한다.
@MappedSuperClass
어노테이션은 실무에서도 많이 사용하고 있기 때문에 잘 설계된 엔티티의 경우
상당히 많은 부모로 부터 라이프 사이클까지 관리될 수 있게 사용되니 잘 기억해두기를 추천한다.👍
- JPA 리뷰 글 보러가기
Leave a comment