관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다.
ORM에서 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것이다.
지금부터 위 그림의 객체 상속 모델을 3가지 방법으로 매핑해보자.
Table
객체는 타입으로 구분할 수 있지만, 테이블은 타입의 개념이 없기 때문에 구분하는 컬럼(DTYPE)을 추가해야 한다.
Entity
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 상속 매핑은 부모 클래스에 사용, 매핑 전략 JOINED 사용
@DiscriminatorColumn(name = "DTYPE") // 구분 컬럼 지정, 저장된 자식 테이블 구분, 기본값이 DTYPE
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
// ...
}
@Entity
@DiscriminatorValue("A") // 엔티티를 저장할 때 구분 컬럼에 입력할 값 지정, DTYPE에 "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") // 부모 테이블의 ID 컬럼명을 변경하고 싶으면 사용
public class Book extends Item {
private String acthor;
private String isbn;
// ...
}
장점
단점
Table
말 그대로 테이블 하나만 사용한다. 주의점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 점이다. (ALBUM 테이블은 ARTIST 컬럼만 사용하기 때문에 나머지 컬럼은 null이 입력된다.)
Entity
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 매핑 전략 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 {
// ...
}
장점
단점
Table
자식 테이블에 필요한 컬럼을 모두 넣는다.
Entity
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PRE_CLASS) // 매핑 전략 TABLE_PRE_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 {
// ...
}
@Entity
public class Movie extends Item {
// ...
}
@Entity
public class Book extends Item {
// ...
}
구분 컬럼을 사용하지 않는다. 일반적으로 추천하지 않는 전략이라고 한다.
장점
단점
@MappedSuperclass
를 사용하면 부모 클래스는 테이블과 매핑하지 않고 자식 클래스에게 매핑 정보를 제공할 수 있다.
Entity
@MappedSuperclass
public abstract class BaseEntity {
@Id
@GeneratedValue
private Long id;
private String name;
// ...
}
@Entity
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")) // 매핑 정보 재정의
public class Member extends BaseEntity {
// ID 상속
// NAME 상속
private String email;
// ...
}
@Entity
@AttributeOverrides({
@AttributeOverride(name = "id", column = @Column(name = "SELLER_ID")),
@AttributeOverride(name = "name", column = @Column(name = "SELLER_NAME"))
}) // 복수로 사용 가능
public class Seller extends BaseEntity {
// ID 상속
// NAME 상속
private String shopName;
// ...
}
@MappedSuperclass
특징
@MappedSuperclass
로 지정한 클래스는 엔티티가 아님(em.find()
나 JPQL 사용 못함)
@MappedSuperclass
를 사용하면 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.
식별 관계
부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계
비식별 관계
부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계
부모 클래스
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
@Column(name = "PARENT_ID1")
private String id1;
@Id
@Column(name = "PARENT_ID2")
private String id2;
private name;
// ...
}
식별자 클래스
public class ParentId implements Serializable {
private String id1;
private String id2;
public ParentId() {
}
public ParentId(String id1, String id2) {
this.id1 = id1;
this.id2 = id2;
}
// equals, hashCode 구현
}
@IdClass
를 사용할 때 식별자 클래스가 만족해야 하는 조건
식별자 클래스는 어떻게 사용될까?
// Parent 엔티티 생성
Parent parent = new Parent();
parent.setId1("myId1");
parent.setId2("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;
}
만약 @JoinColumn
의 name
과 referencedColunmName
의 값이 같다면 생략할 수 있다.
@IdClass
가 데이터베이스에 맞춘 방법이라면EmbeddedId
는 좀 더 객체지향적인 방법이다.
부모 클래스
@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 구현
}
@EmbeddedId
를 사용할 때 식별자 클래스가 만족해야 하는 조건
@Embeddable
어노테이션을 붙여야 함식별자 클래스는 어떻게 사용될까?
// Parent 엔티티 생성
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2"); // ParentId를 사용한다.
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);
// 조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
지금까지 구현한 식별자 클래스를 보면 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) // Override를 했다면 true, 아니면 false
둘은 인스턴스가 다르다. 영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 관리한다. 그리고 식별자를 비교할 때 equals()
와 hashCode()
를 사용한다. 따라서 동등성이 지켜지지 않으면 영속성 컨텍스트가 엔티티를 관리하는 데 심각한 문제를 발생시킬 수 있다.
@EmbeddedId
가IdClass
와 비교해서 더 객체지향적이고 중복도 없어서 좋아보이긴 하지만 특정 상황에서 JPQL이 조금 더 길어질 수 있다.
em.createQuery("select p.id.id1, p.id.id2 from Parent p"); // @EmbeddedId
em.createQuery("select p.id1, p.id2 from Parent p"); // @IdClass
참고: 복합 키에는 @GenerateValue를 사용할 수 없다. 복합 키를 구성하는 여러 컬럼 중 하나에도 사용할 수 없다.
식별 관계는 다음 그림과 같다.
부모
@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 childId; // GrandChild.child 매핑
private String id; // GrandChild.id 매핑
// equals, hashCode
// ...
}
식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다. 식별자 매핑인
@Id
와 연관관계 매핑인@ManyToOne
을 같이 사용하면 된다.
@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("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
// ...
}
@MapsId
는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다.
식별 관계를 비식별 관계로 변경한 그림이다.
이번에는 비식별 관계로 구현해보자.
부모
@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
}
식별 관계와 비교할 때 복합 키를 사용하지 않아 매핑도 쉽고 코드도 간단한 것을 볼 수 있다.
일대일 식별 관계는 특별하다. 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다. 그래서 자식 테이블에서 부모 테이블의 기본 키 값만 사용하고 있다.
부모
@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;
// ...
}