상속관계 매핑
- 관계형 데이터베이스는 상속 관계 X
- 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사
- 상속관계 매핑: 객체의 상속과 구조와 DB의 슈퍼타입 서브타입 관계를 매핑
- 슈퍼타입 서브타입 논리 모델을 실제 물리 모델로 구현하는 방법
- 각각 테이블로 변환 -> 조인 전략
- 통합 테이블로 변환 -> 단일 테이블 전략
- 서브타입 테이블로 변환 -> 구현 클래스마다 테이블 전략
주요 어노테이션
- @Inheritance(starategy=InheritanceType.XXX)
- JOINED: 조인 전략
- SINGLE_TABLE: 단일 테이블 전략
- TABLE_PER_CLASS: 구현 클래스마다 테이블 전략
- @DiscriminatorColumn(name="DTYPE") (default column name => DTYPE)
- @DiscriminatorValue("XXX") (default = entity name)
조인 전략
- 장점
- 테이블 정규화
- 외래키 참조 무결성 제약조건 활용가능
- 저장공간 효율화
- 단점
- 조회시 조인을 많이 사용, 성능 저하
- 조회 쿼리가 복잡함
- 데이터 저장시 INSERT SQL 2번 호출
Example Codes
package hellojpa;
import javax.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // JOIN전략을 사용할때
@DiscriminatorColumn // DTYPE 컬럼을 ITEM Table에 추가
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
// getters / setters
}
@Entity
@DiscriminatorValue("A") // 상속받는 subType 테이블의 이름을 A로 변경 | 기본값은 entity name
public class Movie extends Item{
private String director;
private String actor;
// getters / setters
}
단일 테이블 전략
- 장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
- 조회 쿼리가 단순함
- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 null 허용
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 상황에 따라서 조회 성능이 오히려 느려질 수 있다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn
public class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
// getters / setters
}
구현 클래스마다 테이블 전략
- 이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천X
- 장점
- 서브 타입을 명확하게 구분해서 처리할 때 효과적
- not null 제약조건 사용 가능
- 단점
- 여러 자식 테이블을 함께 조회할 때 성능이 느림(UNION SQL 필요)
- 자식 테이블을 통합해서 쿼리하기 어려움
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn // <- 불필요 어짜피 별개 테이블
public abstract class Item {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
// getters / setters
}
@MappedSuperclass
- 공통 매핑 정보가 필요할 때 사용(id, name)
- 상속관계 매핑X
- 엔티티X, 테이블과 매핑X
- 부모 클래스를 상속 받는 자식 클래스에 매핑 정보만 제공
- 조회, 검색 불가(em.find(BaseEntity) 불가)
- 직접 생성해서 사용할 일이 없으므로 추상 클래스 권장
- 테이블과 관계 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모으는 역할
- 주로 등록일, 수정일, 등록자, 수정자 같은 전체 엔티티에서 공통으로 적용하는 정보를 모을 때 사용
- 참고: @Entity 클래스는 엔티티나 @MappedSuperclass로 지정한 클래스만 상속 가능
@MappedSuperclass
public abstract class BaseEntity {
// mapping 정보를 위한 Superclass 공통 field를 추가하기 위해 사용
private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
// getters / setters
}
복합 키와 식별 관계 매핑
식별 관계 vs 비식별 관계
데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다. 두 관계의 특징을 이해하고 각각을 어떻게 매핑하는지 알아보자.
식별관계(Identifying Relationship)
부모 테이블의 기본 키를 내려 받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계
비식별 관계(Non-Identifying Relationship)
부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계
- 필수적 비식별 관계(Mandatory) : 외래 키에 NULL을 허용하지 않는다. 연관 관계를 필수적으로 맺어야 한다.
- 선택적 비식별 관계(Optional) : 외래 키에 NULL을 허용한다. 연관관계를 맺을지 말지 선택할 수 있다.
복합 키
JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공한다.
@IdClass는 관계형 데이터베이스에 가까운 방법이다.
@EmbeddedId는 좀 더 객체지향에 가까운 방법이다.
@IdClass
복합 키 테이블은 비식별 관계고 PARENT는 복합 기본 키를 사용한다. (객체의 상속과는 무관)
부모 클래스 Code
@Entity
public class Parent {
@Id
@Column(name = "PARENT_ID1")
private String id1; // ParentId.id1과 연결
@Id
@Column(name = "PARENT_ID2")
private String id2; // ParentId.id2와 연결
private String name;
// getters and setters
}
식별자 클래스 Code
public class ParentId implements Serializable {
private String id1; // Parent.id1 매핑
private String id2; // Parent.id2 매핑
public ParentId() {}
public ParentId(String id1, String id2) {
this.id1 = id1;
this.id2 = id2;
}
@Override
public boolean equals(Object obj) {...}
@Override
public int hashCode() {...}
}
Parent parent = new Parent();
parent.setId("myId1"); // 식별자
parent.setId("myId2); // 식별자
parent.setName("parentName");
em.persist(parent);
//복합키 조회 코드
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
자식 클래스 추가
@Entity
public class Child {
@Id
private String id;
@ManyToOne
@JoinColumns({@JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1"),
@JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2")})
private Parent parent;
// getters and setters and constructor
}
@IdClass는 다음 조건을 만족해야 한다.
- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
- (Parent.id1 -> parent.id1, Parent.id2 -> parent.id2)
- Serializable 인터페이스를 구현해야 한다.
- equals, hashCode를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
@EmbeddedId
@IdClass가 데이터베이스에 맞춘 방법이라면 @EmbeddedId는 좀 더 객체지향적인 방법
@Entity
public class Parent {
@EmbeddedId
private String id;
private String name;
// getters and setters
}
@IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.
@Embeddable
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1;
@Column(name = "PARENT_ID2")
private String id2;
// equals and hashCode 구현
}
@EmbeddedId를 적용한 식별자 클래스는 다음 조건을 만족해야 한다.
- @Embeddable 어노테이션을 붙여주어야 한다.
- Serializable 인터페이스를 구현해야 한다.
- equals, hashCode를 구현해야 한다.
- 기본 생성자가 있어야 한다.
- 식별자 클래스는 public이어야 한다.
복합 키와 equals(), hashCode()
복합 키는 equals()와 hashCode()를 필수로 구현해야 한다.
ParentId id1 = new ParentId();
id1.setId1("myId1");
id1.setId2("myId2");
ParentId id2 = new ParentId();
id2.setId1("myId1");
id2.setId2("myId2");
id1.equals(id2) -> ?
equals()를 적절히 오버라이딩했다면 참이지만 equals()를 적절히 오버라이딩하지 않았다면 결과는 거짓이다. 왜냐하면 자바의 모든 클래스는 기본으로 Object 클래스를 상속받는데 이 클래스가 제공하는 기본 equals()는 인스턴스 참조 값 비교인 == 비교 (동일성비교)를 하기 때문이다. 영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리한다. 그리고 식별자 비교에 equals()와 hashCode()를 사용한다. 고로 동등성(equals 비교)이 지켜지지 않으면 영속성 컨텍스트가 엔티티를 관리하는데 심각한 문제가 발생한다.
@IdClass vs @EmbeddedId
각자 일장일단이 있다.
@EmbeddedId가 @IdClass와 비교해서 더 객체지향적이고 중복도 없어서 좋아보이긴 하나 특정 상황에 JPQL이 조금 더 길어질 수 있다.
참고
복합 키에는 @GenerateValue를 사용할 수 없다. 복합 키를 구성하는 여러 칼럼 중 하나에도 사용할 수 없다.
복합 키 : 식별 관계 매핑
식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해서 식별자를 매핑해야 한다.
//부모
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID")
private String id;
private String name;
// getters and setters
}
// 자식
@Entity
@IdClass(ChildId.class)
public class Child {
@Id
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
@Id @Column(name = "CHILD_ID")
private String childId;
private String name;
// getters and setters
}
// 자식ID
public class ChildId implements Serializable {
private String parent; // Child.parent 매핑
private String childId; // Child.childId 매핑
// equals, hashCode ...
}
// 손자
@Entity
@IdClass(GrnadChildId.class)
public class GrandChild {
@Id
@ManyToOne
@JoinColumns({@JoinColumn(name = "PARENT_ID"),@JoinColumn(name = "CHILD_ID")})
private Child child;
@Id @Column(name = "GRNADCHILD_ID")
private String id;
private String name;
// ...
}
// 손자 ID
public class GrandChildId implements Serializable {
private ChildId child; // GrnadChild.child 매핑
private String id; // GrandChild.id 매핑
// equals, hashCode ...
}
@EmbeddedId와 식별 관계
@EmbeddedId로 식별 관계를 구성할 때는 @MapsId를 사용해야 한다.
//부모
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID")
private String id;
private String name;
}
//자식
@Entity
public class Child {
@EmbeddedId
private ChildId id;
@MapsId("parentId") //ChildId.parentId 매핑
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
private String name;
}
// 자식 ID
@Embeddable
public class ChildId implements Serializable {
private String parentId; //@MapsId("parentId")로 매핑
@Column(name = "CHILD_ID")
private String id;
// equals, hashCode ...
}
// 손자
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("child") //GrandChildId.childId 매핑
@ManyToOne
@JoinColumns({@JoinColumn(name = "PARENT_ID"),@JoinColumn(name = "CHILD_ID")})
private Child child;
private String name;
// ...
}
// 손자 ID
@Embeddable
public class GrandChildId implements Serializable {
private ChildId childId; // @MapsId("childId")로 매핑
@Column(name = "GRANDCHILD_ID")
private String id; // GrandChild.id 매핑
// equals, hashCode ...
}
- @IdClass와 다르게 @MapsId를 사용
- @MapsId는 외래키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 의미
- @MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.
비식별 관계로 구현
식별 관계의 복합 키 코드와 비교하여 매핑도 쉽고 코드도 단순
복합키가 없으므로 복합 키 클래스를 만들지 않아도 된다.
//부모
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private String id;
private String name;
// getters and setters
}
//자식
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
private String name;
// ...
}
// 손자
@Entity
public class GrandChild {
@Id @GeneratedValue
@Column(name = "GRNADCHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "CHILD_ID")
private Child child;
// ...
}
일대일 식별 관계
일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다.
//부모
@Entity
public class Board {
@Id @GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
private String title;
@OneToOne(mappedBy = "board")
private BoardDetail boardDetail;
}
// 자식
@Entity
public class BoardDetail {
@Id
private Long boardId;
@MapsId // BoardDetail.boardId 매핑
@OneToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
private String content;
// ...
}
식별, 비식별 관계의 장단점
데이터베이스 설계 관점에서 다음 이유로 비식별 관계를 선호한다.
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하며 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 이는 조인시 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
- 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
- 비즈니스 요구사항은 시간이 지남에 따라 언젠가 변하는데 식별 관계의 자연 키 컬럼들이 자식에 손자까지 전파되면 변경이 힘들다.
- 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하는 식별 관계는 테이블 구조가 유연하지 못함
객체 관계 매핑 관점에서 다음과 같은 이유로 비식별 관계를 선호한다.
- JPA에서 복합 키는 별도의 복합 키 클래스를 만들어 사용해야 함.
- 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 JPA는 @GenerateValue처럼 대리키 생성을 위한 편리한 방법을 제공한다.
식별 관계의 장점
- 기본 키 인덱스를 활용하기 좋다(상위 테이블에서 정의해놓은 인덱스를 그대로 사용할 수 있다.)
- ex) 부모 아이디가 A인 모든 자식 조회 / 부모 아이디가 A고 자식 아이디가 B인 자식 조회
ORM 신규 프로젝트 진행시 추천방법
(필수적[Not Null])비식별 관계 사용 / 기본 키는 Long 타입의 대리 키를 사용
조인 테이블
데이터베이스 테이블의 연관관계 설계 방법은 크게 2가지다.
- 조인 컬럼 사용(외래 키)
- 조인 테이블 사용(테이블 사용)
조인 컬럼 사용
외래 키에 null을 허용하는 선택적 비식별 관계는 회원과 사물함을 조인할 때 외부 조인(OUTER JOIN)을 사용해야 한다. 실수로 내부 조인을 사용하면 사물함과 관계가 없는 회원은 조회되지 않는다. 그리고 회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 값 대부분이 null로 저장되는 단점이 있다.
조인 테이블 사용
이 방법은 조인 테이블이라는 별도의 테이블을 사용해서 연관관계를 관리한다. MEMBER와 LOCKER에는 연관 관계를 관리하기 위한 외래 키 컬럼이 없다. 대신 조인 테이블을 하나 추가해야 한다는 단점이 있다.
일대일 조인 테이블
일대다 조인 테이블
다대일 조인 테이블
다대다 조인 테이블
엔티티 하나에 여러 테이블 매핑
정리
실전예제 - 상속관계 매핑
요구사항 추가
- 상품의 종류는 음반, 도서, 영화가 있고 이후 더 확장될 수 있다.
- 모든 데이터는 등록일과 수정일이 필수다.
식별 관계 상속
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 상속관계 부모
@DiscriminatorColumn // DType 설정
public abstract class Item extends BaseEntity{
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
@ManyToMany(mappedBy = "items")
private List<Category> categories = new ArrayList<>();
// getters and setters
}
@Entity
public class Book extends Item{
private String author;
private String isbn;
// getters and setters
}
공통 속성 상속을 위한 BaseEntity
@MappedSuperclass
public abstract class BaseEntity {
// mapping 정보를 위한 Superclass 공통 field를 추가하기 위해 사용
private String createdBy;
private LocalDateTime createdDate;
private String lastModifiedBy;
private LocalDateTime lastModifiedDate;
// getters and setters
}
References By
참조 문헌 : ORM 표준 JPA 프로그래밍
https://incheol-jung.gitbook.io/docs/study/jpa/7
'BackEnd > ORM(JPA)' 카테고리의 다른 글
값 타입 (0) | 2023.01.09 |
---|---|
프록시와 연관관계 관리 (0) | 2023.01.04 |
다양한 연관관계 매핑 (0) | 2022.12.31 |
연관관계 매핑 기초 (0) | 2022.12.30 |
엔티티 매핑 (0) | 2022.12.29 |