관계형 데이터베이스에서는 상속이란 개념은 없지만, 슈퍼타입 서브타입 관계(Super-Type Sub-Type Relationship)라는 모델링이 상속 개념과 가장 유효하다. 슈퍼타입 서브타입 논리 모델을 테이블로 구현할 떄는 3가지 방법을 선택할 수 있다.
엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다.
테이블은 타입의 개념이 없으므로
DTYPE
컬럼을 구분 컬럼으로 사용한다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 1
@DiscriminatorColumn(name = "DTYPE") // 2
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private String price;
...
}
@Entity
@DiscriminatorValue("A") // 3
public class Album extends Item {
private String artist;
...
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
...
}![](https://velog.velcdn.com/images/yuyun0124/post/85dd6ba5-674d-40c5-bf7d-a7e71376bf8d/image.png)
@Inheritance(strategy = InheritanceType.JOINED)
@Inheritance
를 사용해야 하는데, 여기서는 조인 전략을 사용하므로 nheritanceType.JOINED 사용@DiscriminatorColumn(name = "DTYPE")
DTYPE
이므로 @DiscriminatorColumn
으로 생략해도 된다.@DiscriminatorValue("M")
DTYPE
에 M이 저장된다.기본값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 자식 테이블의 기본 키 컬럼명을 변경하고 싶다면 @PrimaryKeyJoinColumn
을 사용하면 된다.
테이블을 하나만 사용하고, 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분한다.
일반적으로 가장 빠르다고 한다.
@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 String 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 {
...
}
InheritanceType.SINGLE_TABLE
지정 시 단일 테이블 전략을 사용한다. 테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 한다.
@DiscriminatorColumn
)을 꼭 사용해야 한다. 그리고 @DiscriminatorValue
를 지정하지 않으면 기본으로 엔티티 이름을 사용한다.자식 엔티티마다 테이블을 만드는 전략이다.
자식 테이블 각각에 필요한 컬럼이 모두 있다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private String price;
...
}
@Entity
public class Album extends Item {
...
}
@Entity
public class Movie extends Item {
...
}
@Entity
public class Book extends Item {
...
}
일반적으로 추천하지 않는 전략이다.
지금까지는 부모 클래스와 자식 클래스를 모두 데이터베이서 테이블과 매핑했지만, 부모 클래스는 테이블과 매핑하지 않고 자식 클래스에게 매핑 정보만 제공하는 방법도 있다. 바로 @MappedSuperclass
를 사용하는 것이다.
위의 그림에서 회원과 판매자는 서로 관계가 없는 테이블과 엔티티이다. 테이블은 그대로 두고, 객체 모델의 id, name 두 공통 속성을 부모 클래스로 모으고, 객체 상속 관계로 만들 수 있다.
@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;
...
}
BaseEntity
@MappedSuperclass
를 사용하였다.Member
@AttributeOverride
를 사용하여 재정의할수 있다.Seller
@AttributeOverrides
를 사용하면 된다.@AssociationOverrides
나 @AssociationOverride
를 사용하면 된다.@MappedSuperclass
의 특징은 다음과 같다.
@MappedSuperclass
로 지정한 클래스는 엔티티가 아니므로 em.find()
나 JPQL에서 사용 불가테이블과는 관계가 없고, 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할을 한다. ORM에서 이야기하는 진정한 상속 매핑은 위에서 이야기한 슈퍼타입 서브타입 관계와 매핑하면 된다. 하지만 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.
식별 관계
부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계
비식별 관계
부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계
@Entity
public class Hello {
@Id
private String id1;
@Id
private String id2; // 실행 시점에서 매핑 예외 발생
}
복합 키는 다음과 같이 매핑하면 될 것 같지만, 이렇게 사용하게 되면 매핑 예외(org.hibernate.MappingException
)가 발생한다.
영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용하는데, 식별자를 구분하기 위해
equals
와hashCode
를 사용해 동등성 비교를 하는데, 식별자 필드가 하나일 때는 문제가 없지만(보통 자바의 기본 타입을 사용하므로), 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에equals
와hashCode
를 구현해줘야 한다.
관계형 데이터베이스에 가까운 방법이다.
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
@Column(name = "PARENT_ID1")
private String id1;
@Id
@Column(name = "PARENT_ID2")
private String id2;
private String 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;
}
@Override
public boolean equals(Object o) {...}
@Override
public int hashCode() {...}
}
식별자 클래스는 다음 조건을 만족해야 한다.
Parent.id1과 ParentId.id1, Parent.id2과 ParentId.id2
Serializable
인터페이스 구현equals
, hashCode
구현복합 키를 사용해서 저장할 때 ParentId
클래스를 사용하지 않아도 영속성 컨텍스트에서 알아서 생성해서 영속성 컨텍스트의 키로 사용해준다. 조회 시에는 ParentId
를 사용해서 조회할 수 있다.
부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키이다. 따라서 외래 키 매핑시 여러 컬럼을 매핑해야 하므로 @JoinColumns
을 사용하면 된다.
@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
속성과referencedColumnName
속성값이 같으면referencedColumnName
은 생략해도 된다.
@IdClass
와 비교해서 좀 더 객체지향적인 방법이다.
@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
...
}
@IdClass
와는 다르게 식별자 클래스에 기본 키를 직접 매핑한다. 따라서, @IdClass
와 달리 저장시에도 ParentId
를 직접 사용해서 저장을 해주어야 한다. 조회시에는 @IdClass
와 동일하다.
복합 키 사용 시,
equals
와hashCode
를 적절히 오버라이딩해야 영속성 컨텍스트가 엔티티를 적절히 관리 할 수 있다.
@IdClass
와@EmbeddedId
는 각각 장단점이 있으므로 본인의 취향에 맞는 것을 일관성있게 사용하면 좋다.
복합 키에는
@GenerateValue
를 사용할 수 없다.
부모, 자식, 손자까지 계속 기본 키를 전달하는 식별 관계이다. @IdClass
나 @EmbeddedId
를 사용해서 식별자를 매핑해야 한다.
// 부모
@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")
private 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;
...
}
// 손자 ID
public class GrandChildId implements Serializable{
private ChildId child; // 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;
...
}
// 자식
@Entity
public class Child{
@EmbeddedId
private ChildId id;
@MapsId("parentId") // ChildId.parentId 매핑
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
...
}
// 자식 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;
@MapId("childId") // GrandCHildId.childId 매핑
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
...
}
// 손자 ID
@Embeddable
public class GrandChildId implements Serializable{
private ChildId childId; // @MapsId("childId") 매핑
@Column(name = GRANDCHILD_ID)
private String id;
...
}
@IdClass
와의 차이점은 @Id
대신 @MapsId
를 사용하였다는 것이다. @MapsId
의 속성값은 @EmbeddedId
를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.
// 부모
@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;
...
}
@GenerateValue
처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.물론, 식별 관계가 가지는 장점도 존재한다.
따라서, 적절한 상황에서 적절히 사용하는 것이 좋다.
선택적 비식별 관계보다 필수적 비식별 관계를 사용하는 것이 좋다. 선택적 비식별 관계는 NULL을 허용하므로 조인 시 외부 조인을 사용해야 하지만, 필수적 관계는 NOT NULL이므로 항상 관계가 있다는 것을 보장해주기 때문에 내부 조인만 사용해도 된다.
데이터베이스 테이블의 연관관계를 설계하는 방법은 크게 2가지다.
@JoinColumn
으로 매핑한다.@JoinTable
을 사용한다. 주로 다대다 관계를 일대다-다대일 관계로 풀어내기 위해 사용하나, 일대일, 일대다, 다대다 관계에서도 사용한다.조인 테이블의 외래 키 컬럼 각각에 총 2개의 유니크 제약조건을 걸어야 한다.
// 부모
@Entity
public class Parent{
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToOne
@JoinTable(name = "PARENT_CHILD", // 1
joinColumns = @JoinColumn(name = "PARENT_ID"), // 2
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")) //3
private Child child;
...
}
// 자식
@Entity
public class Child{
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
...
}
name
joinColumns
inverseJoinColumns
양방향으로 매핑하려면 @OneToOne(mappedBy="child")
로 매핑하면 된다.
조인 테이블의 컬럼 중 다(N)와 관련된 컬럼에 유니크 제약조건을 걸어야 한다.
// 부모
@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>();
}
// 자식
@Entity
public class Child{
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
...
}
일대다에서 방향만 반대이므로 테이블 모양은 일대다와 같다.
// 부모
@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 = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "CHILD_ID"),
inverseJoinColumns = @JoinColumn(name = "PARENT_ID"))
private Parent parent;
...
}
@ManyToOne(optional = false)
optional 속성을 false로 설정하게 되면 해당 객체에 null이 들어갈 수 없다. 따라서,parent
가 필수적으로 들어가게 되므로 INNER JOIN 쿼리가 생성된다.
다대다 조인 테이블을 만드려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야 한다.
// 부모
@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") // 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 = "BOARD_DETAIL")
private String content;
}
@SecondaryTable.name
@SecondaryTable.pkJoinColumns
더 많은 테이블을 매핑하려면 @SecondaryTables
를 사용하면 된다.
@SecondaryTables({
@SecondaryTable(name = "BOARD_DETAIL"),
@SecondaryTable(name = "BOARD_FILE")
})
두 테이블을 하나의 엔티티에 매핑하는 방법보다 테이블당 엔티티를 각각 만들어 일대일 매핑하는 것을 권장한다. 해당 방법은 항상 두 테이블을 조회하므로 최적화하기 어렵고, 일대일 매핑은 원하는 부분만 조회할 수 있고 필요하면 둘 다 조회하면 되기 때문이다.