1. 상속 관계 매핑
•
관계형 테이터베이스에는 객체지향 언어에서 다루는 상속 개념이 없다.
•
대신 슈퍼타입과 서브타입 관계라는 모델링 기법이 객체의 상속 개념과 가장 유사하다.
슈퍼타입 서브타입 논리 모델
객체 상속 모델
•
슈퍼-서브 타입 논리 모델을 물리 모델로 구현할 때 3가지 방법을 선택할 수 있다.
◦
각각 테이블로 변환 : 각각 테이블을 만들고 조회할때 조인한다.
▪
JPA에서 조인 전략이라고 함.
◦
통합 테이블로 변환 : 테이블을 하나만 사용해서 통합한다.
▪
JPA에서 단일 테이블 전략이라고 함.
◦
서브타입 테이블로 변환 : 서브 타입마다 하나의 테이블로 만든다.
▪
JPA에서 구현 클래스마다 테이블 전략이라고 함.
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;
...
}
// Album 엔티티
@Entity
@DiscriminatorValue("A")
public class Album extends Item{
private String artist;
...
}
// Movie 엔티티
@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
private String director;
private String actor;
...
}
Java
복사
•
장점
◦
테이블이 정규화 된다.
◦
외래 키 참조 무결성 제약조건을 활용할 수 있다.
◦
저장공간을 효율적으로 사용한다.
•
단점
◦
조회할때 조인이 많아 성능 저하될 수 있다.
◦
조회 쿼리가 복잡하다.
◦
테이블을 등록할 때 INSERT SQL을 두 번 실행한다.
•
특징
◦
JPA 표준 명세는 구분 컬럼을 사용하도록 한다.
▪
But. 하이버네이트를 포함한 몇 구현체는 구분 칼럼없이도 동작한다.
•
관련 어노테이션
◦
@PrimaryKeyJoinColumn, @DiscriminatorColumn, @DiscriminatorValue
1.2. 단일 테이블 전략
•
이름 그대로 테이블으 하나만 사용한다.
•
구분 컬럼(DTYPE)을 통해 어떤 자식 테이터가 저장되어있는지 구분한다.
•
조회할 때 조인을 쓰지 않아 가장 빠르다.
@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 {
...
}
Java
복사
•
장점
◦
조인이 필요없어 일반적으로 조회 성능이 빠르다.
◦
조회 쿼리가 단순하다.
•
단점
◦
자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야한다.
◦
모든것을 저장하는게 단일 테이블이니 테이블이 크다.
▪
상황에 따라 조회 성능이 오히려 느려질 수 있다.
•
특징
◦
구분 컬럼이 필수다. = @DiscriminatorColumn
◦
구분 컬럼 값(@DiscriminatorValue)을 지정하지 않으면 엔티티 이름을 그대로 사용한다.
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; //가격
...
}
Java
복사
•
장점
◦
서브 타입을 구분해서 처리할 때 효과적이다.
◦
not null 제약조건을 사용할 수 있다.
•
단점
◦
여러 자식 테이블을 함께 조회할 때 성능이 느리다.
▪
SQL에 UNION을 사용해야 한다.
◦
자식 테이블을 통합해서 쿼리하기 어렵다
•
특징
◦
구분 컬럼을 사용하지 않는다.
2. @MappedSuperclass
•
부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclass를 사용하면 된다.
•
@MappedSuperclass는 비유하자면 추상 클래스와 비슷하다.
◦
@Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과 매핑되지 않는다.
◦
단순히 매핑정보 상속 목적으로만 이용된다.
@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;
...
}
Java
복사
•
BaseEntity에는 객체들이 주로 사용하는 공통 매핑 정보를 정의했다.
◦
자식 엔티티들은 상속을 통해 BaseEntity의 매핑 정보를 물려받았다.
•
부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides나 @AttributeOverride를 사용하고, 연관관계를 재정의하려면 @AssociationOverrides나 @AssociationOverride를 사용한다.
•
특징
◦
테이블과 매핑 되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용한다.
◦
@MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용할 수 없다.
◦
이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장한다.
3. 복합 키와 식별 관계 매핑
3.1. 식별 관계 vs 비식별 관계
•
데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다.
3.1.1. 식별 관계
•
식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계다
3.1.2. 비식별 관계
•
비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.
•
비식별 관계는 외래 키에 NULL을 허용하느지에 딸 필수적 비식별 관계와 선택적 비식별 관계로 나뉜다.
◦
필수적 비식별 관계(Mandatory): 외래 키에 NULL을 허용하지 않는다.
◦
선택적 비식별 관계(Optional): 외래 키에 NULL을 허용한다. (대부분은 비식별 관계로 유지한다)
3.2. 복합 키 : 비식별 관계 매핑
•
JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공한다.
3.2.1. @IdClass
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
@Column(name = "PARENT_ID1")
private String id1; //Parentld.id1과연결
@Id
@Column(name = "PARENT_ID2")
private String id2; //Parentld.id2와연결
private String name;
...
}
@EqualsAndHashCode
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;
}
}
Java
복사
부모 클래스와 식별자 클래스
•
@IdClass를 사용할 때 식별자 클래스는 다음 조건을 만족해야 한다.
◦
식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
◦
Serializable 인터페이스를 구현해야 한다.
◦
equals, hashCode를 구현해야 한다.
◦
기본 생성자가 있어야 한다.
◦
식별자 클래스는 public이여야 한다.
Parent parent = new Parent();
parent.setId("myid1");
parent.setId("myid2");
em.persist(parent);
ParentId parentId = new ParentId("myid1","myid2");
Parent parent = em.find(Parent.class,parent.Id);
Java
복사
복합키 생성과 조회
@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;
...
}
Java
복사
자식 클래스
3.2.2. @EmbeddedId
•
@IdClass가 데이터베이스에 맞춘 방법이라면 @EmbeddedId는 좀 더 객체지향적인 방법이다.
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
private String name;
...
}
Java
복사
@Embeddable
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1;
@Coluinn (name = "PARENT_ID2")
private String id2;
//equals and hashCode 구현
...
}
Java
복사
•
@IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.
•
@EmbeddedId를 적용한 식별자 클래스는 다음 조건을 만족해야 한다
◦
@Embeddable 어노테이션을 붙여주어야 한다.
◦
Serializable 인터페이스를 구현해야 한다.
◦
equals, hashCode를 구현해야 한다.
◦
기본 생성자가 있어야 한다.
◦
식별자 클래스는 public이어야 한다.
// Parent 저장
Parent parent = new Parent();
ParentId parentId = new ParentId("myid1","myid2");
parent.setId(parentId);
em.persist(parent);
// Parent 조회
ParentId parentId = new ParentId("myId1","myId2");
Parent parent = em.find(Parent.class,parentId);
Java
복사
3.2.3. 복합키와 eqauls(), hashCode()
•
복합키는 eqauls()와 hashCode()를 필수로 구현해야 한다.
ParentId id1 = new parentId() ;
id1.setld1("myId1”);
id1.setld2("myId2”);
ParentId id2 = new parentId();
id2.setId1("myId1");
id2.setId2("myId2");
id1.equals(id2) -> ???
Java
복사
•
영속성 컨텍스트는 엔티티의 식별자를 키로 사용해 엔티티를 관리한다.
•
식별자를 비교할 때 eqauls()와 hashCode()를 사용한다.
•
객체의 동등성을 지키지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는 데 심각한 문제가 발생할 수 있다.
3.2.4. @IdClass vs @EmbeddedId
•
@EmbeddedId가 @IdClass와 비교해서 더 객체지향적이고 중복도 없어서 좋아 보이긴 하지만 특정 상황에 JPQL이 조금 더 길어질 수 있다.
em.createQuery("select p.id.id1, p.id.id2 from Parent p"); //@Embeddedld
em.createQuery("select p.id1, p.id2 from Parent p"); //@IdClass
Java
복사
3.3. 복합 키 : 식별 관계 매핑
•
식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해서 식별자를 매핑해야 한다.
3.3.1. @IdClass와 식별 관계
//부모
@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(GrandChildld.class)
public class GrandChild {
@Id
@ManyToOne
@JoinColumns({
@JoinColunm(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
@Id @Column(name = "GRANDCHILD_ID")
private String id;
private String name;
...
}
//손자 ID
public class GrandChildld implements Serializable {
private ChildId child; //GrandChild.child 매핑
private String id; //GrandChild.id 매핑
//equals, hashCode
...
}
Java
복사
•
식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다.
◦
따라서 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 같이 사용하면 된다.
3.3.2. @EmbeddedId와 식별 관계
//부모
@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 GrandChildld implements Serializable {
private Childld childld; //@MapsId(”childld")로 매핑
@Column(name = "GRANDCHILD_ID")
private String id;
//equals, hashCode
...
}
Java
복사
•
@EmbeddedId는 식별 관계로 사용할 연관관계의 속성에 @MapsId를 사용하면 된다.
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;
...
}
Java
복사
3.5. 일대일 식별 관계
•
일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다.
//부모
@Entity
public class Board {
@Id @GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
private String titie;
@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;
...
}
Java
복사
3.6. 식별, 비식별 관계의 장단점
•
데이터베이스 설계 관점에서 보면 다음 이유로 식별 관계보다는 비식별 관계를 선호한다.
◦
식별 관계에서 부모의 기본 키 컬럼은 하나지만 자식은 2개(부모+자식), 손자는 3개(부모,자식,손자)로 점점 기본 키 컬럼 갯수가 늘어난다.
▪
이는 조인할때 더 복잡해지고 기본키 인덱스가 불필요하게 커진다.
◦
식별 관계에서는 2개 이상의 컬럼을 합해 복합 기본키를 만들어야하는 경우가 많다.
◦
식별 관계에서 기본 키로 비지니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많은데 이는 시간이 지남에 따라 비지니스 로직이 바뀌면 전체를 변경하기가 힘들어진다.
◦
식별 관계에서 자식 테이블은 부모 테이블의 기본키를 자식 자신의 기본키로 사용해야하므로 테이블 구조가 유연하지 못하다.
•
객체 관계 매핑 관점에서는 비식별 관계를 선호한다.
◦
일대일 관계를 제외하고 식별 관계에서는 2개 이상의 컬럼을 사용해 복합 키를 사용한다.
▪
JPA에서 복합키를 사용하기 위해서는 복합 키 클래스를 별도로 만들어줘야하는 번거로움이 생긴다.
◦
비식별 관계의 기본 키는 주로 대리키를 사용하는데 JPA에서는 @GeneratedValue처럼 쉽게 대리키를 생성할 수 있다.
•
식별 관계의 장점도 있다.
◦
기본 키 인덱스를 활용하기 쉽고 상위 테이블들의 기본키를 자식,손자들이 가지고 있기때문에 조인 없이 하위 테이블만으로 검색할 수 있다.
•
정리
◦
가능하면 비식별 관계를 사용하고 기본키로 Long 타입의 대리 키를 사용하는것이다.
▪
대리 키는 비지니스 로직과 아무 연관이 없어 유연한 대처가 가능하다.
▪
또한 JPA에서는 @GeneratedValue 처럼 간편하게 대리클 생성할 수 있다.
◦
자바에서 Integer는 범위가 20억 정도인데 Long은 920경 정도여서 Long을 쓰는게 안전하다.
◦
선택적 비식별 관계보다 필수적 비식별 관계를 사용하는것이 좋다.
▪
선택적 비식별 관계는 NULL을 허용해 외부조인을 사용해야하지만 필수적 비식별 관계는 NOT NULL이 보장되므로 내부조인만 사용해도 된다.
4. 조인 테이블
•
데이터베이스 테이블의 연관관계 설계 방법은 크게 2가지다.
◦
조인 컬럼 사용(외래키를 사용하는 방법)
◦
조인 테이블 사용(사이에 테이블을 추가해서 사용하는 방법)
•
조인 컬럼 사용
◦
테이블 간에 관계는 주로 조인 컬럼이라 불리는 왜래 키 컬럼을 사용해서 관린=리한다.
◦
선택적 비식별 관계는 외래 키에 null을 허용하므로 회원과 사물함을 조이니할 때 외부조인을 사용해야 한다.
▪
실수로 내부 조인을 사용할 경우 관계가 없는 회원은 조회되지 않는다.
◦
회원과 사물함이 아주 가끔 관계를 맺는다면 대부분이 null로 저장되는 단점이 있다.
•
조인 테이블 사용
◦
조인 테이블이라는 별도의 테이블을 사용해 연관관계를 관리한다.
◦
조인 컬럼은 단순히 외래 키 컬럼만 추가해 연관관계를 맺지만 조인 테이블을 사용하는 방법은 연관관꼐를 관리하는 조인 테이블을 추가해 여기서 두 테이블의 왜래 키를 가지고 연관관계를 관리한다.
◦
조인 테이블의 가장 큰 단점은 테이블을 하나 추가해야한다는 것이다.
◦
조인 컬럼을 사용하고 필요하다고 판단되면 조인 테이블을 사용하자.
4.1. 일대일 조인 테이블
•
일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼 각각에 총 2개의 유니크 제약조건을 걸어야 한다.
◦
PARNET_ID는 기본 키이므로 유니크 제약조건이 걸려 있다.
//부모
@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 ChiId {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
...
}
Java
복사
4.2. 일대다 조인 테이블
•
일대다 관계를 만들려면 조인 테이블의 컬럼 중 다와 관련된 컬럼인 CHILD_ID에 유니크 제약조건을 걸어야 한다.
// 부모
@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> child = new ArrayList<Child>();
}
// 자식
@Entity
public class Child{
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
...
}
Java
복사
4.3. 다대일 조인 테이블
•
일대다 형태의 반대로 구현해주면 된다.
// 부모
@Entity
public class Parent{
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> child = new ArrayList<Child>();
...
}
// 자식
@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;
...
}
Java
복사
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> child = new ArrayList<Child>();
}
//자식
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
Java
복사
5. 엔티티 하나에 여러 테이블 매핑
•
잘 사용하지는 않지만 @SecondaryTable을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다.
@Entity
@Table(name="BOARD")
@SecondaryTable (name = "BOARD_DETAIL", pkJoinColumns = SPrimaryKeyJoinColumn (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;
...
}
Java
복사
•
@SecondaryTable 속성은 다음과 같다.
•
@SecondaryTable.name : 매핑할 다음 테이블 이름
•
@SecondaryTable.pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성
•
참고로 @SecondaryTable을 사용해서 두 테이블을 하나의 엔티티에 매핑하는 방법보다는 테이블당 엔티티를 각각 만들어서 일대일 매핑하는 것을 권장한다.
◦
이 방법은 항상 두 테이블을 조회하므로 최적화하기 어렵다.