관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없다.
ORM에서 말하는 상속관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입/서브타입 관계를 매핑하는 것이다.
1) 각각의 테이블로 변환 : JPA의 조인전략을 사용하기
2) 통합 테이블로 변환 : 테이블을 하나만 사용하는 단일 테이블 전략
3) 서브타입 테이블로 변환 : 서브타입 마다 하나의 테이블로 만드는 전략

조인전략은 엔티티 각각을 모두 테이블로 만들고 자식테이블이 부모테이블의 기본키를 받아서 기본키+외래키로 사용하는 전략이다. 타입을 구분하는 컬럼을 추가해주어야한다.
부모테이블 코드를 보자.
@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;
...
}
@Inheritance(strategy = InheritanceType.JOINED)@DiscriminatorColumn(name='DTYPE')자식 테이블 코드를 보자.
@Entity
@DisciminatorValue("A")
public class Album extends Item{
private String artist;
...
}
@DiscriminatorValue('M')만약 자식 테이블에서 기본키 컬럼명을 변경하고 싶다면? @PrimaryKeyJoinColumn을 사용하면 된다.
@Entity
@DisciminatorValue("A")
@PrimaryKeyJoinColumn(name="ALBUM_ID") //ID 재정의
public class Album extends Item{
private String artist;
...
}
JPA 표준 명세는 구분 컬럼을 사용하도록 하지만, 하이버네이트를 포함한 몇몇 구현체는 구분컬럼 없이도 동작한다.
@PrimaryKeyJoinColumn, @DiscriminatorColumn, @DisciminatorValue

단일테이블 전략은 테이블을 하나만 사용한다. 그리고 구분 컬럼으로 어떤 자식 데이터가 저장되어있는지 구분한다.
조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.
코드를 보자.
@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
@DisciminatorValue("A")
public class Album extends Item{
...
}
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)를 사용하면 단일 테이블 전략을 사용한다.
@DiscriminatorColumn)@DisciminatorValue를 생략하면 기본으로 엔티티 이름을 사용한다.
자식 엔티티 마다 테이블을 만든다.
코드를 보자.
@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{
...
}
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)를 사용해서 구현 클래스마다 테이블 전략을 사용한다. 일반적으로 추천하지 않는 전략이다.
부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받은 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용한다.
@MappedSurperClass 는 실제 테이블과는 매핑되지 않는다.

회원과 판매자는 서로 관계가 없는 테이블이다. id, name 두 공통 속성만 부모 클래스로 모아준 BaseEntity 를 생성하였다.
코드를 보자.
@MappedSuperclass
public class BaseEntity {
@Id
@GeneratedValue
private Long id;
private String name;
...
}
@Entity
public class Member7_2 extends BaseEntity {
private String email;
}
...
@MappedSuperClass 를 사용해서 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하였다.
참고
@Entity 는 @Entity 이거나 @MappedSuperclass로 지정한 클래스만 상속받을 수 있다.
식별관계는 부모 테이블의 기본키를 내려받아서 자식 테이블의 기본키 + 외래키로 사용하는 관계다.
비식별관계는 부모 테이블의 기본키를 받아서 자식 테이블의 외래키로만 사용하는 관계다. 최근에는 비식별관계를 주로 사용한다.
JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야한다.
JPA는 복합키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공하는데, @IdClass는 관계형 데이터베이스에 가까운 방법이고, @EmbeddedId는 좀 더 객체지향에 가까운 방법이다.

PARENT 테이블을 보면 기본키를 PARENT_ID1, PARENT_ID2 로 묶은 복합키로 구성했다.
@Entity
@IdClass(ParentId.class)
public class Parent{
@Id
@Column(name = "PARENT_ID1")
private String id; // ParentId.id1과 연결
@Id
@Column(name = "PARENT_ID2")
private String id2; // ParentId.id2와 연결
private String name;
...
}
public class ParentId implements Serializable {
private String id;
private String id2;
public ParentId() {
}
public ParentId(String id, String id2) {
this.id = id;
this.id2 = id2;
}
//equals, hashCode 오버라이드
...
}
@IdClass 를 이용한 식별자 클래스 조건
그러면 실제 저장을 어떻게 하는지 보자.
Parent parent = new Parent();
parent.setId("myId1"); //식별자
parent.setId("myId2"); //식별자
parent.setName("parentName");
em.persist(parent);
저장 코드를 보면, ParentId가 보이지 않는다. em.persist() 를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2를 사용해서 ParentId를 생성하고 영속성 컨텍스트의 키로 사용한다.
조회 코드를 보자.
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
식별자 클래스인 ParentId를 통해서 조회를 한다.
이제 자식 클래스 코드를 보자.
@Entity
public class Child{
@Id
private Stirng id;
@ManyToOne
@JoinColumns({
@JoinColumn(name="PARENT_ID1", referecedColumnName = "PARENT_ID1"),
@JoinColumn(name="PARENT_ID2", referecedColumnName = "PARENT_ID2")
})
private Parent parent;
}
외래키 매핑시 여러 컬럼을 매핑해야하므로 @JoinColumns 어노테이션을 사용한다.
참고로, @JoinColumn 의 name과 referencedColumnName 속성의 값이 같으면 referencedColumnName은 생략해도 된다.
@EmbeddedID 는 좀 더 객체지향적인 방법이다.
@Entity
public class Parent{
@EmbeddedId
private ParentId id;
private String name;
...
}
Parent 엔티티에서 식별자 클래스(ParentId)를 직접 사용하고 @EmbeddedId 어노테이션을 적어주면 된다.
@Embeddable
public class ParentId implements Serializable{
@Column(name="PARENT_ID1")
private String id;
@Column(name="PARENT_ID2")
private String id2;
//equals and hashCode 구현
...
}
@IdClass와는 다르게 @EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본키를 직접 매핑한다.
@EmbeddedId를 적용한 식별자 클래스 조건
@EmbeddedId를 사용한 저장 코드
Parent parent =new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId);
parent.setName("아무개");
em.persist();
식별자 클래스 parentId를 직접 생성해서 사용한다.
@EmbeddedId를 사용한 조회 코드
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
조회 코드도 식별자 클래스 parentId를 직접 사용한다.
복합키는 equals()와 hashCode()를 필수적으로 구현해야한다.
왜냐하면, 영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리하고, 식별자를 비교할 때 equals()와 hashCode()를 사용하기 때문이다.
@EmbeddedID 가 @IdClass와 비교해서 좀 더 객체지향적일 수 있고, 중복도 없어서 좋아보이지만, 상황에 따라서 JPQL이 더 길어줄 수 있다.
em.createQuery("select p.id.id1, p.id.id2 from Parent p"); //@Embedded
em.createQuery("select p.id, p.id2 from Parent p"); //@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
...
}
식별관계는 기본키와 외래키를 같이 매핑해야 한다. 따라서 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne 을 같이 사용하였다.
코드를 보자.
//부모
@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 Parent2 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 구현
...
}
@IdClass 와 다른점은 @Id 대신에 @MapsId를 사용하였다.
@MapsId 는 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 뜻이다.
6장에 나왔던 예처럼 식별관계를 비식별관계로 변경하자.

//부모
@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 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;
...
}
BoardDetial처럼 식별자가 단순히 하나면 @MapsId 를 사용하고 속성 값은 비워두면 된다.
이때 @MapsId는 @Id를 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑된다.
데이터베이스 테이블의 연관관계를 설계하는 방법은 조인컬럼(외래키)을 사용하거나 조인테이블을 사용하는 방법이 있다.
테이블 간에 관계는 주로 조인 컬럼이라 부르는 외래키 컬럼을 사용해 관리한다.

회원이 아직 사물함을 사용하지 않으면 둘 사이의 관계가 없으므로, MEMBER테이블의 LOCKER_ID 외래키에 NULL을 허용하여야한다.(선택적 비식별관계)
따라서 두 테이블을 조회할 때 외부 조인으로 사용해야한다.

회원과 사물함 데이터를 각각 등록했다가 회원이 원할 때 사물함을 선택하면 MEMBER_LOCKER 테이블에만 값을 추가하면 된다.
단점은, 테이블을 추가로 관리해야하고, 회원과 사물함 두 테이블을 조인하려면 MEMBER_LOCKER 테이블까지 추가로 조인해야한다.

일대일 관계를 만들기 위해 조인 테이블의 외래키 컬럼각각에 2개의 유니크 제약조건을 걸어야한다.
코드를 보자.
//Parent
@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;
...
}
//Child
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
조인테이블을 이용해서 단방향 일대일 매핑을 하였다.
@JoinTable의 속성
name : 매핑할 조인 테이블 이름
joinColumns : 현재 엔티티를 참조하는 외래키
inverseJoinColumns: 반대방향 엔티티를 참조하는 외래키
참고로 나는 위코드를 Mysql DB로 설정해서 실행했더니, child_id 에 대해서 유니크 제약조건이 생성되지 않고, MUL타입으로 생성되었다.
MUL타입이란?
MULTIPLE 의 줄인 말로, 다른 테이블의 기본 키를 참조하는 외래키를 나타낸다. 해당 값은 인덱스로 등록되고 여러행이 동일한 값을 가질 수 있다.

일대다중 다에 해당하는 CHILD컬럼에 유니크 제약조건이 걸려야한다.(기본키여서 유니크제약조건이 걸려있음)
//Parent
@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>();
...
}
//Child
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
}
조인테이블을 이용해서 단방향 일대다 매핑을 하였다.

다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야한다.
//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>();
...
}
//Child
@Entity
public class Child {
@Id
@GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
...
}
잘 사용하지는 않지만 @SecondaryTable을 사용하면 한 엔티티에 여러테이블을 매핑할 수 있다.

코드를 보자.
//Parent
@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 = "BAORD_DETAIL")
private String content;
...
}
@SecondaryTable을 사용해서 BOARD_DETAIL 테이블을 추가로 매핑했다.
@SecondaryTable 속성