[JPA] 7장 - 고급매핑
7장-고급매핑
자바 ORM 표준 JPA 프로그래밍 7장을 요약한 내용 입니다.
이 장에서 다룰 고급 매핑
- 상속 관계 매핑: 객체의 상속 관계에 대한 데이터베이스 표현
@MappedSupperClass
: 등록일, 수정일 같이 여러 공통으로 사용하는 매핑 정보만 상속받고 싶을때- 복합 키와 식별 관계 매핑: 데이터베이서의 식별자(id) 값이 하나 이상일때 매핑하는 방법
- 조인 테이블: 연관관계를 관리하는 연결 테이블(매핑 테이블)을 두는 방법
- 엔티티 하나에 여러 테이블을 매핑하기
7.1 상속 관계 매핑
관계형 데이터베이스에는 상속이라는 개념이 없다.
대신에 아래 그림과 같은 슈퍼타입-서브타입 관계(super type - sub type Relationship)이라는 모델링 기법이 있다.
슈퍼-서브 타입의 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택할 수 있다.
- 각각의 테이블로 변환
- 하나의 테이블로 변환
- 서브타입 테이블로 변환
7.1.1 조인 전략
조인 전략은 아래 그림과 같이 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다. 타입을 구분하기 위한 컬럼이 필요하다.
image
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
}
기본 값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶다면 @PrimaryKeyJoinColumn
을 사용하자
장점
- 테이블이 정규화 된다
- 외래 키 참조 무결성 제약조건을 활용할 수 있다
- 저장공간을 효율적으로 사용한다
단점
- 조회할때 성능이 저하될 수 있다
- 조회 쿼리가 복잡하다
- 데이터를 등록할때 insert sql이 두 번 실행된다
7.1.2 단일 테이블 전략
아래 그림과 같이 테이블 하나를 사용하는 전략
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다
- 조회 쿼리가 단순하다
단점
- 자식 엔티티가 매핑한 컬럼은 모두 NULL 허용해야 한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다
7.1.3 구현 클래스마다 테이블 전략
각각의 엔티티마다 테이블을 만든다
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
각각의 자식 엔티티마다 테이블을 만들기 때문에 일반적으로 추천되지 않는다
장점
- 서브 타입을 구분해서 처리할 때 효과적이다
NOT NULL
제약조건을 사용할 수 있다
단점
- 여러 자식테이블과 함께 조회할 때 성능이 느리다(UNION 사용이 불가피)
- 자식 테이블을 통합해서 쿼리하기 어렵다
7.2 @MappedSuperclass
부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶을때 사용한다
추상클래스와 비슷하다.(엔티티는 실제 테이블과 매핑된다)
@MappedSuperclass
public abstract class BaseEntity{
@Id
private Long id;
private String name;
}
@Entity
public class Member extends BaseEntity {
// id, name 상속
}
@Entity
public class Seller extends BaseEntity{
// id, name 상속
}
부모로 부터 물려 받은 매핑정보를 재정의 하려면 @AttributeOverride
를 사용한다.
특징
- 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용한다.
@MappedSuperclass
로 지정한 클래스는 엔티티가 아니므로em.find()
나 Jpql에서 사용할 수 없다- 직접적으로 생성할일이 적기 때문에 추상클래스로 만드는것이 적합하다
⭐ 등록일자, 수정일자, 등록자, 수정자와 같은 여러 엔티티를 효율적으로 관리할 수 있다?
다음과 같은 상황에는 어떨까?
- 등록일시가 필요한 상황
- 등록일시, 수정일시가 필요한 상황
- 등록일시, 등록자가 필요한 상황
- 등록일시, 등록자, 수정일시가 필요한 상황
- 등록일시, 등록자, 수정일시, 수정자가 필요한 상황
이렇게 요구사항이 다양할 경우 상속 보다는 합성(composition)을 통한 구현이 더 좋지 않을까?
jpa에서 합성은 Embedded objects로 구현되어 있다.
7.3 복합 키와 식별 관계 매핑
7.3.1 식별 관계 vs 비식별 관계
식별 관계
식별 관계는 부모 테이블의 기본 키를 내려받아서 자식테이블의 기본 키 + 외래 키로 사용하는 관계다
비식별 관계
부모의 테이블의 기본 키를 받아서 자식 테이블의 외래키로만 사용하는 관계다.
최근 추세는 비식별 관계를 주로 사용하고 필요한 경우 식별 관계를 사용하는 추세다.
7.3.2 복합 키: 비식별 관계 매핑
jpa는 둘 이상의 컬럼으로 구성된 복합 기본키 매핑을 위해서 @IdClass
와 @EmbeddedId
를 지원한다
@IdClass
@Entity
@IdClass(ParentId.class)
public class Parent{
@Id
private String id1;
@Id
private String id2;
}
public class ParentId implements Serializable {
private String id1;
private String id2;
}
식별자 클래스는 다음 조건을 만족해야 한다
- 식별자 클래스의 속성명과 엔티티에서 사용하는 속성명이 같아야 한다.
Serializable
인터페이스를 구현해야한다.equals
,hashcode
를 구현해야한다
@Entity
public class Child {
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1),
@JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2)
})
private Parent parent;
}
부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블 역시 복합 키로 매핑해 주어야 한다
@EmbeddedId
조금 더 객체지향적인 방법
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
}
@Embeddable
public class ParentId implements Serializable {
private String id1;
private String id2;
}
사용하기 위한 조건
Embeddable
annotation을 붙여주어야 한다.Serializable
interface를 구현해야한다equals
,hashCode
를 구현해야한다. → 오버라이딩 하지 않는 다면 == 비교(동일성 비교)를 하기 때문에 의도한대로 동작하지 않을 수 있다
@IdClass
vs @EmbeddedId
EmbeddId가 더 객체지향적일 수는 있지만 특정 상황에 jpql이 더 길어질 수 있다.
7.3.2 복합 키: 식별 관계 매핑
부모, 자식, 손자까지 계속 기본 키를 전달하는 관계
@Entity
public class Parent {
@Id
private String id;
}
@Entity
@IdClass(CHildId.class)
public class Child {
@Id
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
@Id
private String childId;
}
public class ChildId implements Serializable {
private String parent; // Child.parent 매핑
private String childId; // Child.childId 매핑
}
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id
@ManyToOne
@JoinColumns({
@JoinColumn(name ="PARNET_ID")
@JoinColumn(name = "CHILD_ID")
})
private Long id;
@ManyToOne
private Child child;
}
이처럼 식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다. 식별자 매핑인 @Id
와 연관관계 매핑인 @ManyToOne
을 같이 사용하면 된다
@EmbeddedId
와 식별 관계
@Entity
public class Parent {
@Id
private String id;
}
@Entity
public class Child {
@EmbeddedId
private ChildId id;
@MapsId("parentId")
@ManyToOne
public Parent parent;
}
@Embeddable
public class ChildId implements Serializable {
private String parentId; // @MapsId("parentId")로 매핑
private String id;
}
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("childId") // GrandChildId.childId
@JoinColumns({
@JoinColumn(name = "PARENT_ID")
@JoinColumn(name = "CHILD_ID")
})
private Child child;
}
@Embeddable
public class GrandChildId implements Serializable {
private ChildId childId; // @MapsId("childId")로 매핑
}
7.3.4 비식별 관계로 구현
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
@Entity
public class GrandChild {
@Id @GeneratedValue
@Column(name = "GRANDCHILD_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "CHILD_ID")
private Child child;
}
7.3.5 일대일 식별 관계
@Entity
public class Board {
@Id @GeneratedValue
private Long id;
@OneToOne(mappedBy = "board")
private BoardDetail boardDetail;
}
@Entity
public class BoardDetail {
@Id
private Long id;
@MapsId // BoardDetail.boardId 매핑
@OneToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
}
7.3.6 식별, 비식별 관계의 장 단점
일반적으로 비식별 관계를 선호한다.
- 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다
- 식별 관계와는 다르게 비즈니스와 전혀 관계 없는 값을 키 값으로 사용한다
- 테이블 구조가 유연하다
- 복합 키를 만들기 위해서는 많은 노력이 필요하다
물론 식별 키가 지니는 장점도 있다.
- 키 인덱스를 활용하기 좋고
- 특정 상황에서 조인 없이 하위테이블을 조회할 수 있다.
7.4 조인 테이블
데이터베이스에서 테이블간의 연관관계를 설계하는 방법은 크게 2가지다
- 조인 컬럼 사용(외래 키)
- 조인 테이블 사용(테이블 사용)
조인 컬럼 사용
LOCKER_ID
는 NULL 값이 들어가는 경우가 많은 선택적 비식별 관계이다
조인 테이블 사용
- 조인 테이블의 단점은 테이블을 추가해야된다는 것이다.
- 필요에 따라서 사용하는 것이 좋다
7.4.1 일대일 조인 테이블
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
@OneToOne
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private Child child;
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
}
@JoinTable
속성
- name: 매핑할 조인 테이블 이름
- joinColumns: 현재 엔티티를 참조하는 외래 키
- inverseJoinColumns: 반대방향 엔티티를 참조하는 외래 키
7.4.2 일대다 조인 테이블
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
@OneToMany
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private Child child;
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
}
7.4.3 다대일 조인 테이블
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> child;
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
@ManyToOne(optional = false)
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "CHILD_ID"),
inverseJoinColumns = @JoinColumn(name = "PARENT_ID")
)
private Parent parent;
}
7.4.4 다대다 조인 테이블
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
@ManyToOne
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private List<Child> child;
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
}
7.5 엔티티 하나에 여러 테이블 매핑
@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board {
@Id @GeneratedValue
private Long id;
@Column(table = "BOARD_DETAIL") // 테이블 지정을 통해 특정 테이블의 컬럼 정보를 매핑
private String content
}
@SecondaryTable
보다는 두 테이블을 각각의 엔티티에 매핑후 일대일 연관관계로 만드는 것을 권장한다