관계형 DB는 객체지향 언어에서 다루는 상속
이라는 개념이 없다..!
대신 슈퍼타입 서브타입 관계
라는 모델링 기법이 객체의 상속 개념과 가장 유사하다
슈퍼타입 서브타입 논리 모델을 실제 물리모델인 테이블로 구현할 때는 아래와 같은 방법들이 있다
자식 테이블이 부모테이블의 기본 키를 받아서 '기본 키+외래 키'로 사용하는 전략
객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없어 타입을 구분하는 컬럼을 추가해야한다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
...
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item {
private String artist;
...
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {
private String director;
private String actor;
...
}
default = DTYPE
DTYPE
에 M이 저장됨자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 @PrimaryKeyJoinColumn
을 사용
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {
private String author;
private String isbn;
...
}
Book 테이블의 ITEM_ID 기본 키 컬럼명을 BOOK_ID로 변경하였다.
@DiscriminatorValue
, @PrimaryKeyJoinColumn
, @DiscriminatorColumn
테이블을 하나만 사용하는 전략, 조회시 조인을 사용하지 않아 가장 빠르다
자식 엔티티가 매핑한 컬럼은 모두 nullable 해야한다
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
...
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item { ... }
@Entity
@DiscriminatorValue("M")
public class Movie extends Item { ... }
@Entity
@DiscriminatorValue("B")
public class Book extends Item { ... }
테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 함
자식 엔티티마다 테이블을 만들며 각각 필요한 컬럼이 모두 존재
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id
@GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
...
}
@Entity
public class Album extends Item { ... }
@Entity
public class Movie extends Item { ... }
@Entity
public class Book extends Item { ... }
구현 클래스마다 테이블 전략 사용, DB설계자와 ORM 전문가 둘 다 비추천하는 전략
부모클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶을 때 사용
@Entity
는 실제 테이블과 매핑, @MappedSuperclass
는 단순히 매핑정보를 상속
매핑정보를 재정의 : @AttributeOverrides
, @AttributeOverride
연관관계를 재정의 : @AssociationOverrides
, @AssociationOverride
@Entity
@AttributeOverrides({
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")),
@AttributeOverride(name = "name", column = @Column(name = "MEMBER_NAME"))
})
public class Member extends BaseEntity { ... }
둘 이상을 재정의하려면 위와 같이 하면 된다 !
@MappedSuperclass
특징
복합 키 지원 어노테이션
@IdClass
: 관계형 데이터베이스에 가까운 방법
@EmbeddedId
: 객체지향에 가까운 방법
@IdClass
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
@Column(name = "PARENT_ID1")
private String id1;
@Id
@Column(name = "PARENT_ID2")
private String id2;
...
}
// 식별자 클래스
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) { ... }
@Overrid
public int hashCode() { ... }
}
식별자 클래스는 아래의 조건을 만족해야 한다
Parent.id1
, ParentId.id1
... etcParent parent = new Parent();
parent.setId1("myId1");
parent.setId2("myId2");
parent.setName("parentName");
em.persist(parent);
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 String id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID1",
referencedColumnName = "PARENT_ID1"),
@JoinColumn(name = "PARENT_ID2",
referencedColumnName = "PARENT_ID2")
})
private Parent parent;
}
부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키다.
외래 키 매핑시 여러 컬럼을 매핑해야 하므로 @JoinColumns
어노테이션을 사용하고 각각의 외래 키 컬럼을 @JoinColuimn
으로 매핑
@JoinColumn
의 name
속성과 referencedColumnName
속성의 값이 같으면 생략가능
@EmbeddedId
@Entity
@IdClass(ParentId.class)
public class Parent {
@EmbeddedId
private String id;
...
}
// 식별자 클래스
@Embeddable // jobda-database -> ThemeCompanyReivew에서 사용중인 어노테이션
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1;
@Column(name = "PARENT_ID2")
private String id2;
//equals and hashCode 구현
...
}
@EmbeddedId
를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑하며 아래와 같은 조건을 만족해야 한다
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);
식별자 클래스 parentId를 직접 생성해서 사용
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
식별자 클래스 parentId를 직접 사용
복합 키와 equals()
, hashCode()
ParentId id1 = new parentId();
id1.setid1("myId1");
id1.setid2("myId2");
ParentId id2 = new parentId();
id2.setid1("myId1");
id2.setid2("myId2");
id1.equals(id2) -> ????
id1.equals(id2)
는 참? 거짓?
equals()
를 적절히 오버라이딩 했다면 참이겠지만 그 반대의 경우 거짓이다.equals()
는 인스턴스 참조 값 비교인 == 비교이기 때문@IdClass
vs @EmbeddedId
EmbeddedId가 더 객체지향적이고 중복도 없어 좋아보이긴 하지만 특정 상황에따라 JPQL이 조금 더 길어질 수 있다.
✅ 복합 키에는 `@GenerateValue` 를 사용할 수 없다.식별관계에서 자식 테이블은 부모 테이블의 기본 키를 포함해서 복합 키를 구성해야하므로 @IdClass
나 @EmbeddedId
를 사용해서 식별자를 매핑해야 한다
@IdClass와 식별 관계
식별 관계는 기본 키와 외래 키를 같이 매핑
//부모
@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;
...
}
// 자식 Id
public class ChildId implements Serializable {
private String parent;
private String childId;
//equals and 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;
...
}
Child 엔티티의 parent 필드를 보면 @Id로 기본키를 매핑하면서 @ManyToOne
과 @JoinColumn
으로 외래키를 같이 매핑
@EmbeddedId와 식별 관계
//부모
@Entity
public class Parent {
@Id
@Column(name = "PARENT_ID")
private String id;
...
}
@Entity
@IdClass(ChildId.class)
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 and hashCode 구현
...
}
// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("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;
// eqauls, hashCode
...
}
식별 관계로 사용할 연관관계의 속성에 @MapsId
를 사용하면 됨!
@IdClass
와 다른점은 @MapsId
를 사용한다는 것인데 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 뜻
식별관계에 비해 매핑도 쉽고 코드도 단순
복합키가 없으므로 복합키 클래스를 만들지 않아도 됨
// 부모
@Entity
public class Board {
@Id
@GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
@OneToOne(mappedBy = "board")
private BoardDetail boardDetail;
...
}
// 자식
@Entity
public class BoardDetail {
@Id
private Long boradId;
@MapsId // BoardDetail.boardId
@OneToOne
@JoinColumn(name = "BOARD_ID")
private Board board;
...
}
BoardDetail 처럼 식별자가 단순히 컬럼 하나면 @MapsId
를 사용하고 속성값은 비워두면 됨
@MapsId
는 @Id
를 사용해서 식별자로 지정한 BoardDetail.boardId와 매핑
식별 <<<< 비식별 선호 이유?
식별관계도 장점은 있다!
SELECT * FROM CHILD
WHERE PARENT_ID = 'A' AND CHILD_ID = 'B'
기본 키 인덱스를 PARENT_ID + CHILD_ID로 구성하면 별도의 인덱스를 생성할 필요 없이 기본 키 인덱스만 사용해도 된다 !
연관관계를 설정하는 방법
일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼 각각에 총 2개의 유니크 제약조건을 걸어야 함
@OneToOne
@JoinTable(name = "PARENT_CHILD",
joinColumns = @JoinColumn(name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
)
private Child child;
...
}
// 양방향 매핑을 하고싶다면
public class Child {
...
@OneToOne(mappedBy = "child")
private Parent parent;
}
일대다 관계를 만들려면 조인 테이블의 컬럼 중 다(N)와 관련된 컬럼인 CHILD_ID에 유니크 제약조건을 걸어야 함
어노테이션만 @OneToMany
방법은 동일
다대일은 일대다에서 방향만 반대
어노테이션만 @ManyToOne
, 조인 테이블 모양은 일대다와 같다
다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 복합 유니크 제약조건을 걸어야함
어노테이션만 @ManyToMany
, 방법 동일
@SecondaryTable
을 사용하여 여러테이블 매핑이 가능
@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board {
...
@Column(table = "BOARD_DETAIL")
private String content;
}