RDBMS에선 상속이 없는 대신 슈퍼타입-서브타입 관계 Super-Type Sub-Type Relationship
가 있다. ORM의 상속 관계 매핑은 이를 객체의 상속 구조와 매핑하는 것이다. 다음 세 가지 방법이 있다.
엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모의 기본 키를 받아 외래키와 매핑해 사용한다.
조회할 때 많이 사용한다.
객체와 달리 테이블엔 타입의 개념이 없기 때문에, 타입을 구분하는 컬럼을 추가해야 한다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 조인 전략을 사용하겠다는 의미
@DiscriminatorColumn(name = "DTYPE") // 부모 클래스에 구분 컬럼 지정, 기본값이 DTYPE
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
....
}
@Entity
@DiscriminatorValue("M") // 구분 컬럼에 입력할 값 지정
@PrimaryKeyJoinColumn(name = "MOVIE_ID") // ID 재정의
public class Movie extends Item {
private String director;
private String actor;
....
}
조인 전략의 장점은
단점으로는
JPA 표준 명세에는 구분 컬럼을 명시하도록 하지만, 하이버네이트를 포함한 몇몇 구현체에선 구분 컬럼이 생략 가능하다.
하나의 통합 테이블로 변환하는 방식을 말한다.
위 코드와 비슷한데, @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
로 지정해주고 자식 엔티티 매핑 컬럼은 null을 허용해줘야 한다.
단일 테이블 전략의 장점은
단점으로는
구분 컬럼이 필수이며 만약 지정하지 않을 경우 엔티티 이름이 기본적으로 사용된다.
이외에도 서브타입마다 하나의 테이블로 변환하는 방식인 구현 클래스마다 테이블 전략@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
이 있지만, SQL에 UNION을 사용하는 등 성능 저하에 문제가 있어 추천하지 않으시댄다.
자식 클래스만 테이블과 매핑하고 이들이 상속하는 부모 클래스를 활용하고 싶을 때 사용한다. 추상클래스 개념과 비슷하고, 즉 매핑 정보를 상속할 목적으로만 사용한다.
@MappedSuperclass // 응 난 테이블이랑 매핑 안해~ 내 자식들이랑 매핑하던가 해 난 클래스야~
public abstract class BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
}
@Entity
@AttributeOverrides({
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID"))
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID"))
}) // 부모에게 물려받은 매핑 정보를 변경 가능하고, 이렇게 복수가 아니라 단일로도 가능하다.
public class Member extends BaseEntity {...}
테이블 사이에 관계는 외래키가 기본 키에 포함되는지에 따라 식별 관계와 비식별 관계를 구분할 수 있다.
식별관계 : 부모 테이블의 기본 키를 받아 자식 테이블의 기본키 + 외래 키로 사용하는 관계
비식별관계 : 부모 테이블의 기본 키를 받아 자식 테이블의 외래 키로만 사용하는 관계
여기서 비식별 관계는 외래 키에 NULL 허용 여부에 따라 또 둘로 나뉜다.
Mandatory
: NULL 허용 X, 연관관계를 필수로 맺어야 함Optional
: NULL 허용. 선택적 연관관계.책에서는 필수적 비식별 관계를 추천하는데, NULL을 허용하는 선택적 비식별 관계는 외부 조인을 사용해야 하기 때문이다.
참고로, 복합키에는 @GeneratedValue
를 사용할 수 없고, 복합키를 구성하는 여러 컬럼에서도 사용할 수 없다.
식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 거기서 equals, hashCode를 구현해야 한다.
JPA에서 2가지 방법을 활용해 볼 수 있다.
RDBMS에 특화된 방식이다.
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id @Column(name = "PARENT_ID_FIRST")
private String first;
@Id @Column(name = "PARENT_ID_SECOND")
private String second;
}
public class ParentId implements Serializable {
private String first; private String second; // 각각 Parent 클래스 아이디와 매핑
public ParentId() {
}
public ParentId(String first, String second) {
this.first = first;
this.second = second;
}
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
@Entity
public class Child {
@Id
private String id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID_FIRST",
referencedColumnName = "PARENT_ID_FIRST"),
@JoinColumn(name = "PARENT_ID_SECOND",
referencedColumnName = "PARENT_ID_SECOND"),
})
private Parent parent;
}
여기서, 식별자 클래스(ParentId)의 속성명과 엔티티(Parent)에서 사용하는 식별자의 속성명이 같아야 한다.
Parent 엔티티의 기본 키 컬럼이 복합 키이므로 Child 테이블의 외래 키도 복합 키이다.
따라서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns을 사용하는데, JoinColumn의 name과 referencedColumnName이 같으면 생략 가능하다.
객체 지향적인 방법이다.
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
...
}
@Embeddable
public class ParentId implements Serializable {
@Column(name = "PARENT_ID_FIRST")
pravate String first;
@Column(name = "PARENT_ID_SECOND")
pravate String second;
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
식별자 클래스에 기본 키를 직접 매핑한다. 보다 객체지향적이고 중복이 없어 좋아보이지만, JPQL이 길어질 수 있는 단점이 존재한다.
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID")
private String id;
...
}
@Entity
@IdClass(ChildId.class)
public class Child {
@Id @ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
@Id @Column(name = "CHILD_ID")
private String childId;
...
}
public class ChildId implements Serializable {
private String parent; // Child.parent 매핑
private String childId; // Child.childId 매핑
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id @ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
public Child child;
@Id @Column(name = "GRANDCHILD_ID")
private String id;
...
}
public class GrandChildId implements Serializable {
private Child child; // GrandChild.child 매핑
private String id; // GrandChild.id 매핑
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
식별 관계는 기본 키와 외래 키를 같이 매핑해야 하기 때문에 @ManyToOne 사용하면 된다.
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID")
private String id;
...
}
@Entity
public class Child {
@EmbeddedId
private ChildId id;
@MapsId("parentId") // ChildId.parentId 매핑
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
...
}
@Embeddable
public class ChildId implements Serializable {
private String parentId; // @MapsId("parentId")로 매핑
@Column(name = "CHILD_ID")
private String id;
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("childId") // GrandChildId.childId 매핑
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
public Child child;
...
}
@Embeddable
public class GrandChildId implements Serializable {
private ChildId childId; // @MapsId("childId")로 매핑
@Column(name = "GRANDCHILD_ID")
private String id;
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
외래키와 매핑한 연관관계를 기본키에도 매핑하기 위해 @Id 대신 @MapsId를 사용한다. @MapsId의 속성값은 @EmbeddedId를 사용한 식별자 클래스(ChildId)의 기본 키 필드를 지정하면 된다.
RDMBS 관점에서 비식별 관계의 장점은
객체지향 관점에서 비식별 관계의 장점은
식별 관계의 장점은 상속을 통해 특정 상황에서 자손 테이블로 부모 테이블의 정보를 검색할 수 있다는 점이다.
객체와 테이블을 연결할 때 조인 컬럼은 @JoinColumn, 조인 테이블은 @JoinTable을 활용한다.
@JoinTable은
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
@OneToOne
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private Child child;
...
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
...
}
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
@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;
...
}
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> child = new ArrayList<Child>();
...
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
@ManyToOne(optional = false)
@JoinTable(name = "PARENT_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;
@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;
...
}
만약 조인 테이블에 컬럼을 추가하면 @JoinTable 전략을 사용할 수 없고 새로운 엔티티를 만들어 조인 테이블과 매핑해야 한다.
@SecondaryTable 어노테이션으로 한 엔티티에 여러 테이블을 매핑할 수 있다. (잘 사용 x)
@Entity
@Table(name = "BOARD") // BOARD 테이블과 매핑
@SecondaryTable(
name = "BOARD_DETAIL",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID")
) // 추가로 BOARD_DETAIL 테이블 매핑
public class Board {
...
@Column(table = "BOARD_DETAIL") // BOARD_DETAIL 테이블의 컬럼 매핑
private STring content;
}
김영한, 『자바 ORM 표준 JPA 프로그래밍』 에이콘(2015)