1. 상속 관계 매핑
- 관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없고,
- 대신 상속과 유사한 슈퍼타입 서브타입 관계라는 모델링 기법이 있다.
- 상속 관계 매핑은 객체의 상속구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것
- 조인 전략 : 각각을 모두 테이블로 만들고 조회할 때 조인 사용
- 단일 테이블 전략 : 테이블을 하나만 사용해 통합
- 구현클래스마다 테이블 전략 : 서브타입마다 하나의 테이블을 만들어 사용
1.1 조인 전략
- 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본키를 받아서 기본키+외래키로 사용
- 테이블은 타입의 개념이 없기 때문에 타입을 구분하는 컬럼을 추가해야 함
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract 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;
}
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {
private String author;
private String isbn;
}
- 상속매핑은 부모 클래스에
@Inheritance
를 사용해야 한다- 조인 전략을 사용하므로 매핑 전략
strategy = InheritanceType.JOINED
를 지정
- 조인 전략을 사용하므로 매핑 전략
@DiscriminatorColumn
으로 부모 클래스에 구분 컬럼을 지정한다- 기본값
DTYPE
- 하이버네이트를 포함한 몇 구현체는 구분 컬럼 없이도 동작
- 기본값
@DiscriminatorValue
로 엔티티를 저장할 때 구분 컬럼에 입력할 값을 저장한다.- 만약 자식 테이블의 기본 키 컬럼을 변경하고 싶으면
@PrimaryKeyJoinColumn
를 사용- 예 :
@PrimaryKeyJoinColumn(name = "BOOK_ID")
name
속성을 통해 자식 테이블의 기본 키 컬럼명을 지정한다.name
속성을 사용하지 않으면 부모 테이블의 컬럼명을 그대로 사용한다.
- 예 :
장점
- 테이블이 정규화 됨
- 외래 키 참조 무결성 제약조건을 활용할 수 있음
- 저장공간을 효율적으로 사용
단점
- 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있음
- 조회 쿼리가 복잡해짐
- 데이터를 등록할 INSERT SQL이 두 번 실행됨
1.2 단일 테이블 전략
- 테이블을 하나만 사용하고 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분한다.
- 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract 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 {
...
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
...
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
}
- 매핑전략
InheritanceType.SINGLE_TABLE
를 지정 - 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
- 만약 Book 엔티티를 저장하면 Item 테이블의 AUTHOR, ISBN 컬럼만 사용하고 다른 엔티티와 매핑된 컬럼은 사용하지 않으므로 null 이 입력된다.
@DiscriminatorColumn
로 구분 컬럼을 꼭 지정해야 한다.@DiscriminatorValue
를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.
장점
- 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다
- 조회 쿼리가 단순하다
단점
- 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
- 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다.
- 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.
1.3 구현 클래스마다 테이블 전략
- 자식 엔티티마다 테이블을 만들고 테이블 각각에 필요한 컬럼이 모두 있다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
}
@Entity
public class Album extends Item {
...
}
public class Movie extends Item {
...
}
@Entity
public class Book extends Item {
...
}
- 매핑 전략
InheritanceType.TABLE_PER_CLASS
를 지정 - 구분 컬럼을 사용하지 않는다.
- 일반적으로 추천하지 않는 전략이므로 조인이나 단일 테이블 전략을 고려하는 것이 낫다.
장점
- 서브 타입을 구분해서 처리할 때 효과적이다
- not null 제약조건을 사용할 수 있다
단점
- 여러 자식 테이블과 함께 조회할 때 성능이 느리다 (SQL에 UNION을 사용해야 함)
- 자식 테이블을 통합해서 쿼리하기 어렵다.
2. @MappedSuperclass
지금까지의 상속 관계 매핑은 부모 클래스와 지식 클래스를 모두 데이터베이스 테이블과 매핑했다.
부모 클래스는 테이블과 매핑하지 않고, 상속받는 자식클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclass
를 사용하면 된다.
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
}
@Entity
public class Member extends BaseEntity {
// ID 상속
// NAME 상속
private String email;
...
}
@Entity
public class Seller extends BaseEntity {
// ID 상속
// NAME 상속
private String shopName;
...
}
- Member와 Seller의 공통 속성 id, name을 부모 클래스인 BaseEntity에 모음
- 자식 엔티티들이 상속을 통해 BaseEntity의 매핑 정보를 물려받음
- BaseEntity는 테이블과 매핑할 필요가 없고 매핑 정보만 제공하면 되기 때문에
@MappedSuperclass
를 사용 @MappedSuperclass
로 지정한 클래스는 엔티티가 아니므로em.find()
나 JPQL에서 사용할 수 없다.- 직접 생성해서 사용할 일은 없으므로 추상 클래스로 만드는 것을 권장
- 자식 엔티티에서 재정의
- 매핑 정보 재정의 :
@AttributeOverride
,@AttributeOverrides
- 연관관계 재정의 :
@AssociationOverride
,@AssociationOverrides
- 매핑 정보 재정의 :
3. 복합 키와 식별 관계 매핑
3.1 식별 관계 vs 비식별 관계
식별 관계 : 부모 테이블의 기본 키를 내려받아 자식 테이블의 기본키+외래키로 사용하는 관계
비식별 관계 : 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계
- 필수적 비식별 관계 : 외래 키에 NULL을 허용하지 않음 (연관관계 필수)
- 선택적 비식별 관계 : 외래 키에 NULL 허용
최근에는 비식별 관계를 주로 사용하고 필요한 곳에만 식별 관계를 사용하는 추세이다.
3.2 복합 키 : 비식별 관계 매핑
JPA는 복합 키를 지원하기 위해 @IdClass
와 @EmbeddedId
2가지 방법을 제공한다.
@IdClass
는 관계형 데이터베이스에 가까운 방법이고 @EmbeddedId
는 좀 더 객체지향에 가까운 방법이다.
@IdClass
- PARENT 테이블은 복합 기본 키 사용 → 복합 키를 매핑하기 위해 식별자 클래스 필요
@IdClass(ParentId.class)
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;
...
}
@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;
}
public class ParentId implements Serializable {
private String id1;
private String id2;
// 기본 생성자
// equals
// hashCode
}
- 부모의 기본 키 컬럼이 복합키 이므로 자식 테이블의 외래 키도 복합키이다.
- 여러 컬럼을 매핑해야하므로
@JoinColumns
사용 - 각각의 외래 키 컬럼을
@JoinColumn
로 매핑
- 여러 컬럼을 매핑해야하므로
- 식별자 클래스의 속성 명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
@EmbeddedId
- Parent 클래스에서 식별자 클래스를 직접 사용하고
@Embeddable
를 적어주면 됨
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
private String name;
}
@Embeddable
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1;
@Column(name = "PARENT_ID2")
private String id2;
// 기본 생성자
// equals
// hashCode
}
@IdClass, @EmbeddedId 사용 시 식별자 클래스 조건
Serializable
인터페이스를 구현해야 한다.
Serializable (직렬화)
: 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 byte 형태의 데이터로 변환하는 기술
equals
,hashCode
를 구현해야 한다.- 기본 생성자가 있어야 한다.
- 식별자 클래스는
public
이어야 한다.
복합 키와 equals(), hashCode()
- 영속성 컨텍스트는 엔티티의 식별자를 키로 사용하여 관리한다.
equals()
와hashCode()
를 통해 비교할 때 식별자 객체의 동등성이 지켜지지 않으면 문제가 발생한다.- 따라서
equals()
와hashCode()
를 적절히 오버라이딩해서 구현해야 한다.
3.3 복합 키 : 식별 관계 매핑
@IdClass와 식별 관계
- 식별 관계는 기본키와 외래키를 같이 매핑해야 하기 때문에 식별자 매핑인
@Id
와 연관관계 매핑인@ManyToOne
을 같이 사용
// 부모
@Entity
public class Parent {
@Id
@Column(name = "PARENT_ID")
private String id;
private String name;
}
// 자식
@Entity
@IdClass(ChildId.class)
public class Child {
@Id
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
@Id
@Column(name = "CHILD_ID")
private String childId;
private String name;
}
// 자식 ID
public class ChildId implements Serializable {
private String parent; // Child.parent 매핑
private String childId; // Child.childId 매핑
// 기본 생성자
// equals
// hashCode
}
// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID")
@JoinColumn(name = "CHILD_ID")
})
private Child child;
@Id
@Column(name = "GRANDCHILD_ID")
private String id;
private String name;
}
// 손자 ID
public class GrandChildId implements Serializable {
private ChildId child; // GrandChild.child 매핑
private String id; // GrandChild.id 매핑
}
@EmbeddedId와 식별 관계
@EmbeddedId
로 식별 관계를 구성할 때는@Id
대신@MapsId
를 사용@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("childId") // 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;
// 기본 생성자
// equals
// hashCode
}
3.4 비식별 관계로 구현
@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
}
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
@Entity
public class GrandChild {
@Id
@GeneratedValue
@Column(name = "GRANDCHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "CHILD_ID")
private Child child;
}
- 식별 관계보다 매핑도 쉽고 코드도 간단하다
3.5 일대일 식별 관계
@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;
}
- 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용
- 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블도 복합 키 X
- 식별자가 단순히 컬럼 하나면
@MapsId
를 사용하고 속성값은 비워두면 된다.
3.6 식별관계와 비식별관계의 장단점
식별 관계는 기본 키 인덱스를 활용하기 좋고, 특정 상황에 조인 없이 하위 테이블만으로 검색할 수 있는 장점이 있지만 아래와 같은 단점들로 인해 식별 관계보다 비식별 관계를 선호한다
- 데이터베이스 설계 관점
- 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다. 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다
- 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야하는 경우가 많다.
- 식별 관계에서 기본 키로 사용하는 자연 키 컬럼들이 자식에 손자까지 전파되면 나중에 비지니스 요구사항이 변경되었을 때 대처가 어렵다
- 객체 관계 매핑의 관점
- 식별관계는 일대일 매핑을 제외하고는 복합 키를 사용한다. JPA에서 복합 키는 컬럼이 하나인 기본 키를 매핑하는 것보다 많은 노력이 필요하다
- 비식별 관계의 기본 키는 주로 대리 키를 사용하는데, JPA는 대리 키를 생성하기 위한 편리한 방법을 제공한다.
💡추천 : 비식별 관계를 사용하고 기본 키는 Long 타입의 대리키를 사용하는 것
4. 조인 테이블
데이터베이스 테이블의 연관관계를 설계하는 방법
- 조인 컬럼 (외래키 사용)
- 조인 테이블 (테이블 사용)
4.1 일대일 조인 테이블
- 조인 테이블의 외래 키 컬럼 각각에 총 2개의 유니크 제약 조건을 걸어야 함
@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@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;
private String name;
}
@JoinTable
사용name
: 매핑할 조인 테이블 이름joincolumns
: 현재 엔티티를 참조하는 외래 키inverseJoinColumns
: 반대방향 엔티티를 참조하는 외래 키
4.2 일대다 조인 테이블
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany
@JoinTable(name = "PARENT_CHILD",
joincolumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
4.3 다대일 조인 테이블
- 일대다에서 방향만 반대
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@ManyToOne(optional = false)
@JoinTable(name = "PARENT_CHILD",
joincolumns = @JoinColumn(name = "CHILD_ID"),
inverseJoinColumns = @JoinColumn(name = "PARENT_ID")
)
private Parent parent;
}
4.4 다대다 조인 테이블
- 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약 조건을 걸어야 함
@Entity
public class Parent {
@Id
@GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@ManyToMany
@JoinTable(name = "PARENT_CHILD",
joincolumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
4.5 엔티티 하나에 여러 테이블 매핑
@SecondaryTable
을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다.
@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board {
@Id
@GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
private String title;
@Column(table = "BOARD_DETAIL")
private String content;
}
@SecondaryTable
name
: 매핑할 다른 테이블의 이름pkJoinColumns
: 매핑할 다른 테이블의 기본 키 컬럼 속성
'자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[자바 ORM 표준 JPA 프로그래밍] 9장 - 값 타입 (0) | 2023.07.22 |
---|---|
[자바 ORM 표준 JPA 프로그래밍] 8장 - 프록시와 연관관계 관리 (0) | 2023.06.13 |
[자바 ORM 표준 JPA 프로그래밍] 6장 - 다양한 연관관계 매핑 (0) | 2023.05.15 |
[자바 ORM 표준 JPA 프로그래밍] 5장 - 연관관계 매핑 기초 (0) | 2023.05.01 |
[자바 ORM 표준 JPA 프로그래밍] 4장 - 엔티티 매핑 (0) | 2023.05.01 |