부모 클래스는 테이블과 매핑하지 않고, 부모 클래스를 상속 받는 자식 클래스에게
매핑 정보만 제공하고 싶으면 @MappedSuperclass를 사용하면 된다.
@Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과는 매핑되지 않는다.
회원과 판매자는 서로 관계가 없는 테이블과 엔티티다. 회원과 판매자의 공통 속성인 id,name을 정의한 부모 클래스를 생성하고 객체 상속 관게로 만들 수 있다.
@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;
...
}
부모로부터 물려받은 매핑 정보를 재정의하거나, 연관관계를 재정의 하려면 다음과 같은 애너테이션을 사용해서 재정의 할 수 있다.
// 하나의 속성을 재정의
@Entity
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")
public class Member extends BaseEntity { ... }
// 둘 이상의 속성을 재정의
@Entity
@AttributeOverrides({
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")),
@AttributeOverride(name = "name" column = @Column(name = "MEMBER_NAME"))
})
public class Member extends BaseEntity { ... }
데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분한다.
식별 관계는 부모 테이블의 기본키를 내려 받아서 자식 테이블의 기본 키 + 외래키로 사용하는 관계다.
식별관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래키로 사용 관계다.
비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계다.
비식별 관계는 외래키에 NULL을 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 나뉜다.
최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세다.
JPA는 식별관계와 비식별 관계를 모두 지원한다.
기본 키를 구성하는 컬럼이 하나면 아래 코드와 같이 단순하게 매핑한다.
@Entity
public class Hello {
@Id
private String id;
}
둘 이상의 컬럼으로 구성된 복합 기본키는 아래와 같이 매핑하면 오류가 발생한다.
@Entity
public class Hello {
@Id
private String id1
@Id
private String id2;
}
JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.
PARENT 테이블은 기본 키를 PARENT_ID1, PARENT_ID2로 묶은 복합키로 구성하였다.
// @IdClass를 사용한 복합키 매핑 예제 - 부모 클래스
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
@Coulumn(name = "PARENT_ID1")
private String id1; // ParentId.id1과 연결
@Id
@Coulumn(name = "PARENT_ID2)
private String id2 // ParentId.id2 연결
private String name;
...
}
// @IdClass를 사용한 식별자 클래스 지정예제
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() { ... }
}
// 복합 키를 사용하는 엔티티를 저장하는 예제
Parent parent = new Parent();
parent.setId1("myId1");
parent.setId2("myId2");
parent.setNmae("parentName");
em.persist(parent);
em.persist( )를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용하기 때문에, 식별자 클래스인 ParentId가 코드에서 보이지 않는다.
// 복합 키를 사용하여 조회하는 예제
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;
}
부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키다.
외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns 애너테이션을 사용하고,
각각의 외래 키 컬럼을 @JoinColumn으로 매핑한다.
@JoinColumn의 name 속성과 referencedColumnName 속성의 값이 같으면 referencedColumnName은 생략해도 된다
@IdClass가 데이터베이스에 맞춘 모양이라면 @EmbededId는 좀 더 객체지향적인 방법이다.
@Entity
public class Parent {
@EmbededId
private ParentId id;
private String name;
...
}
Parent 엔티티에서 식별자 클래스를 직접 사용하고 @EmbededId 애너테이션을 적어주면 된다.
@Embeddable
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1;
@Column(name = "PARENT_ID2")
private String id2;
// eqauls and hashCode 구현
}
// @EmbeddedId를 사용하는 예제
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1","myId2");
parent.setId(parentid);
parent.setName("parentName");
em.persist(parent);
복합 키는 equals()와 hashCode()를 필수로 구현해야 한다.
@EmbeddedId가 @IdClass와 비교해서 더 객체지향적이고 중복도 없어서 좋아보이긴 하지만, 특정상황에 JPQL이 조금 더 길어질 수 있다.
// @EmbeddedId를 사용한 JPQL
em.createQuery("select p.id.id1, p.id.id2 from Parent p");
// @IdClass를 사용한 JPQL
em.createQuery("select p.id1, p.id2 from Parent p");
복합 키에는 @GeneratedValue를 사용할 수 없다. 복합 키를 구성하는 여러 컬럼 중 하나에도 사용할 수 없다.
// @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 chlidId; // Child.childId 매핑
// equals, hashCode
...
}
// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id
@ManyToOne
@JouinColumns({
@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 매핑
// equals, hashCode
...
}
// 부모
@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
@JoinColum(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
...
}
// 비식별 관계 매핑 예제
// 부모
@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;
...
}
// 일대일 식별 관계 저장 예제
public void save() {
Board board = new Board();
board.setTitle("제목");
em.presist(board);
BoardDetail boardDetail = new BoardDetail();
boardDetail.setContent("내용");
boardDetail.setBoard(board);
em.persist(boardDetail);
}
부모 아이디가 A인 모든 자식 조회
SELECT * FROM CHILD WHERE PARENT_ID = 'A';
부모 아이디가 A고 자식 아이디가 B인 자식 조회
SELECT * FROM CHILD WHERE PARENT_ID = 'A' AND CHILD_ID = 'B';
식별관계가 가지는 장점도 분명히 있지만, 될 수 있으면 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것이 좋다.
따라서 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하는 것이 좋다.