ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것!
자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략
🚨 주의할 점
타입을 구분하는 컬럼을 추가해야 한다. 여기서는 DTYPE 컬럼을 구분 컬럼으로 사용한다.
@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")
@PrimaryKeyJoinColumn(name = "MOVIE_ID") // 자식 테이블의 기본 키 컬럼명을 변경 (기본 값은 부모 테이블의 ID 컬럼명)
public class Movie extends Item {
private String director; //감독
private String actor; //배우
...
}
🔻 장점
🔻 단점
🔻특징
JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분 컬럼(@DiscriminatorColumn) 없이도 동작한다.
이름 그대로 테이블을 하나만 사용한다. 그리고 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다. 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.
🚨 주의할 점
자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
@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
@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; //가격
...
}
🔻 장점
🔻 단점
🔻 특징
부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @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;
...
}
부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides나 @AttributeOverride를 사용하고, 연관관계를 재정의하려면 @AssociationOverrides나 @AssociationOverride를 사용한다.
🔻 특징
식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계다.
비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.
JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공하는데 @IdClass는 관계형 데이터베이스에 가까운 방법이고 @EmbeddedId는 좀 더 객체지향에 가까운 방법이다.
@IdClass
복합 키 테이블은 비식별 관계고 PARENT는 복합 기본 키를 사용한다. 여기서 말하는 부모와 자식은 객체의 상속과는 무관하다.
@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;
...
}
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 o) {...}
@Override
public int hashCode() {. ..}
}
여기서 자식 클래스를 추가해보자
@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;
...
}
@EmbeddedId
@IdClass가 데이터베이스에 맞춘 방법이라면 @EmbeddedId는 좀 더 객체지향적인 방법이다.
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
private String name;
...
}
복합 키와 equals(), hashCode()
복합 키는 quals()와 hashCode()를 필수로 구현해야 한다.
ParentId id1 = new parentId() ;
id1.setld1("myId1”);
id1.setld2("myId2”);
ParentId id2 = new parentId();
id2.setId1("myId1");
id2.setId2("myId2");
id1.equals(id2) -> ?
equals()를 적절히 오버라이딩 했다면 참이겠지만 equals()를 적절히 오버라이딩 하지 않았다면 결과는 거짓이다. 자바의 모든 클래스는 기본 equals()는 인스턴스 참조 값 비교인 == 비교(동일성 비교)를 하기 때문이다. 따라서 객체의 동등성(equals 비교)이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는 데 심각한 문제가 발생한다.
@IdClass vs @EmbeddedId
@EmbeddedId가 @IdClass와 비교해서 더 객체지향적이고 중복도 없어서 좋아 보이긴 하지만 특정 상황에 JPQL이 조금 더 길어질 수 있다.
식별 관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해서 식별자를 매핑해야 한다.
@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
...
}
@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("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
...
}
비식별 관계로 만든 테이블을 매핑해보자
//부모
@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 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;
...
}
🔻 데이터베이스 설계 관점에서 비식별 관계를 선호하는 이유
🔻 객체 관계 매핑 관점에서 비식별 관계를 선호하는 이유
데이터베이스 테이블의 연관관계를 설계 방법은 크게 2가지가 있다.
🔻 조인 컬럼 사용
선택적 비식별 관계는 외래 키에 null을 허용 하므로 회원과 사물함을 조인할 때 외부 조인(OUTER JOIN)을 사용해야 한다. 실수로 내부 조인을 사용하면 사물함과 관계가 없는 회원은 조회되지 않는다. 그리고 회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 값 대부분이 null로 저장되는 단점이 있다.
🔻 조인 테이블 사용
이 방법은 조인 테이블이라는 별도의 테이블을 사용해서 연관관계를 관리한다. 따라서 MEMBER와 LOCKER에는 연관관계를 관리하기 위한 외래 키 컬럼이 없다. 단점은 조인 테이블을 하나 추가해야 한다는 점이다.
일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼 각각에 총 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;
...
}
일대다 관계를 만들려면 조인 테이블의 컬럼 중 다와 관련된 컬럼인 CHILD_ID에 유니크 제약조건을 걸어야 한다.
다대일은 일대다에서 방향만 반대이다
//부모
@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 = HPARENT_CHILD",
joinColumns = @JoinColumn(name = "CHILD_ID"),
inverseJoinColumns = @JoinColumn(name = "PARENT_ID")
)
private Parent parent;
}
다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야 한다.
//부모
@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;
}
조인 테이블에 컬럼을 추가하면 @JoinTable 전략을 사용할 수 없다. 대신에 새로운 엔티티를 만들어서 조인 테이블과 매핑해야 한다.
잘 사용하지는 않지만 @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;
...
}
@SecondaryTable 속성은 다음과 같다.
@SecondaryTable을 사용해서 두 테이블을 하나의 엔티티에 매핑하는 방법보다는 테이블당 엔티티를 각각 만들어서 일대일 매핑하는 것을 권장한다. 이 방법은 항상 두 테이블을 조회하므로 최적화하기 어렵다. 반면 일대일 매핑은 원하는 부분만 조회할 수 있고 필요하면 둘을 함께 조회하면 된다.