RDB는 객체지향 언어처럼 상속이라는 개념이 없다.
대신 슈퍼타입 서브타입 관계라는 모델링 기법이 있는데, 이게 상속 개념과 가장 유사하다.
아래는 슈퍼타입 서브타입 관계의 전략들이다.
각각의 테이블로 변환(조인 전략)
엔티티 각각(자식, 부모 전부)을 테이블로 만들고,
자식 테이블이 부모의 기본키를 받아서 기본키 + 외래키로 사용하는 방법이다.
가장 객체지향에 가깝게 생기긴 했다.
장점
테이블이 정규화된다
외래키 참조 무결성 제약 조건 사용 가능
저장 공간을 효율적으로 사용함
단점
조회할 때 항상 조인해서 들고와야함
등록할 때 INSERT를 항상 2번 실행해야함
DTYPE을 지정한 것은 탐색의 범위를 줄위기 위함이다(아마도?)
(부모 테이블 전체를 탐색하는 것은 낭비이기 때문이다)
DTYPE은 데이터베이스 관점에서 무조건적으로 필요한 컬럼이다.
album -> item 으로 접근할때는 상관없지만, item 에서 접근할때는 DTYPE이 없으면 어느 테이블에 조인할 지 전혀 모르기 때문이다.
DTYPE을 통해서 if문을 넣을수도 있고, 뷰모델에서 뿌리는 걸 나눠줄수도 있다.
JPA 표준 명세에서는 이를 지정해주도록 하지만 하이버네이트는 굳이 지정해주지 않아도 잘 동작한다.
/**
* 엔티티 정의
**/
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 1
@DiscriminatorColumn(name = "DTYPE") // 2
public abstract class Item{
@Id
@GeneratedValue
private Integer id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("A") // 3
public class Album extends Item{
private String author;
}
@Entity
@DiscriminatorValue("M") // 3
@PrimaryKeyJoinColumn(nane = "MOVIE_ID")
public class Movie extends Item{
private String director;
private String actor;
}
/**
* 등록, 조회
**/
public void save(){
Album album = new Album();
// set...
em.persist(album);
}
public void select(){
Album album = em.find(Album.class, 1);
}
사용하는 부분은 별로 다를 것 없다.
상속 매핑을 사용할 것이고, 조인 전략을 사용할 것이라는 의미이다.
자식 테이블을 구분할 컬럼이다. 실제 테이블의 컬럼으로 생성된다. 기본값이 DTYPE 이다.
구분 컬럼에 저장될 값이다.
기본적으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 이를 바꿔주고 싶을 때 사용한다.
실행결과
-- 삽입
INSERT INTO ITEM(id, name, price, DTYPE) VALUES(1, '앨범', 10000, 'A');
INSERT INTO ALBUM(id, author) VALUES(1, '소녀시대');
INSERT INTO ITEM(id, name, price, DTYPE) VALUES(1, '인셉션', 10000, 'M');
INSERT INTO MOVIE(id, director, actor) VALUES(1, '크리스토퍼 놀란', '디카프리오');
-- 조회
select
*
from
album album0_
inner join
item album0_1_
on album0_.id=album0_1_.id
where
album0_.id = 1
-- and i.DTYPE = 'A'
hibernate에서는 조건절에 따로 DTYPE이 추가되지 않았다
통합 테이블로 변환(단일 테이블 전략)
전략 이름 그대로 하나의 테이블에 다 때려넣는 전략이다.
그리고 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다.
조인 전략과 달리 자식 -> 부모 쪽으로 접근할 수 있는 방법이 없으므로 구분 컬럼은 필수로 사용해야한다.
장점
조인이 필요없다
단점
자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 오히려 성능이 느려질 수 있다.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 1
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item{
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item{
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
}
단일 테이블 전략을 사용할 것이라는 의미이다.
실행 결과
-- 삽입
INSERT INTO ITEM(id, name, price, artist, DTYPE) VALUES(1, '앨범', 10000, '소녀시대', 'A');
-- 조회
select
*
from
item album0_
where
album0_.id=1
and album0_.DTYPE='A'
서브타입 테이블로 변환(구현 클래스마다 테이블 전략)
자식 엔티티마다 테이블을 만드는 전략이다.
자식 테이블에 필요한 컬럼이 모두 있다. 구분 컬럼이 필요없다.
장점
not null 제약조건을 사용할 수 있다.(별로 장점도 아님. 조인 전략도 not null 사용 가능함)
단점
여러 자식 테이블을 함께 조회할 때 성능이 느리다(UNION을 사용해야함)
자식 테이블을 통합해서 쿼리하기 어렵다
데이터베이스 설계자와 객체지향 설계자 둘 다 추천하지 않는 방법이다.
조인이나 단일 테이블 전략을 고려하는 것이 좋다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 1
public abstract class Item{
@Id
@GeneratedValue
private Long id;
private String name;
private int price;
}
@Entity
@DiscriminatorValue("A")
public class Album extends Item{
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
}
구현 클래스마다 테이블 전략을 사용하겠다는 의미이다. DTYPE이 필요없다.
매핑 정보만 상속(@MappedSupperClass)
부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶을 경우 사용한다.
단순히 매핑 정보만 상속할 목적으로 사용한다.
@MappedSuperClass // 1
public abstract class BaseEntity{
@Id
@GeneratedValue
private Long id;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
}
@Entity
class Member extends BaseEntity{
// ...
}
@Entity
@AttributeOverride(name = "id", column = @Column(name = "TEAM_ID")) // 2
class Team extends BaseEntity{
// ...
}
BaseEntity는 테이블과 매핑되지 않고 단순히 자식 엔티티에게 매핑 정보만 제공하는 용도로 사용된다.
(ORM에서 말하는 진정한 상속 매핑은 처음 설명했던 상속 관계 매핑을 말한다)
매핑 정보만 제공할 클래스라는 의미이다.
매핑정보를 재정의 하고 싶을 경우 사용한다. 여러개를 지정하고 싶을 경우 @AttributeOverrides를 사용한다.
위에 명시하진 않았지만 관계를 재정의 하고 싶을 경우 @AssociationOverride를 사용한다.
근데 이것보다 그냥 @Embeddable을 쓰는게 나을 것 같다. @MappedSuperClass는 추상 클래스만이 가능한데, 다중 상속이 안되는 자바에서 단순히 매핑 정보를 추가 정의하기 위해 상속을 써버리는 것은 좋지 않은 것 같다..
일단 위에는 createdDate, lastModifiedDate로 작성했지만, 이렇게 사용하는게 좋은 예시는 아닌 것 같다.
복합키 매핑
JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.
그냥 자바 기본 타입 2개 쓰고 @Id 선언하면 안된다.
JPA에서 별도의 식별자 클래스를 만드는 방법은 2가지가 있다.
두 방식의 장단점이 있으니, 원하는 방식을 선택해서 일관성 있게 하나만 사용하는 것이 좋다.
@IdClass
@IdClass를 이용한 복합키 선언은 아래와 같다.
@Entity
@IdClass(ParendId.class)
public class Parent{
@Id
@Column(name = "PARENT_ID1")
private String id1;
@Id
@Column(name = "PARENT_ID2")
private String id2;
}
@NoArgsConstructor
@AllArgsConstructor
public class ParentId implements Serializable{
private String id1; // Parent.id1 에 대한 정보 제공
private String id2; // Parent.id2 에 대한 정보 제공
// equals, hashCode
}
@IdClass가 정보 제공용도(식별자 정보는 여기를 참고해라) 정도로 쓰이고 있다.
@IdClass로 사용된 식별자 클래스는 아래 조건을 만족해야 한다.
식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 함
Entity에 매핑 정보를 적고, IdClass에서 해당 변수명에 맞춰 정보를 제공해주고 있다.
아래 식별/비식별 관계에서 복합키 사용 매핑하는 부분에서 더 상세히 볼 수 있다.
Serializable 인터페이스 구현해야 함
equals, hashCode 구현해야함
기본 생성자 필요
식별자 클래스는 public 이어야 함
실제 사용은 아래와 같다.
// save
public void save(){
Parent parent = new Parent();
parent.setId1("id1");
parent.setId2("id2");
em.persist(parent);
}
// select
public void select()}{
ParentId parentId = new ParentId("id1", "id2");
Parent foundParent = em.find(Parent.class, parentId);
}
저장 코드에 식별자 클래스인 ParentId가 보이지 않는 이유는 em.persist를 호출하면 JPA가 내부에서 Parent.id1, Parent.id2 값을 이용해서 ParentId를 생성하고 영속성 컨텍스트의 키로 사용하기 때문이다.
이 부분만 보면 확실히 RDB에 가까운 방법이긴 하다.
하지만 조회쪽을 보면 또 그렇지도 않음.. 결국 ParentId를 생성해서 조회하고 있다.
@EmbededId
@IdClass보다 좀 더 객체지향적인 방법이다.
@Entity
public class Parent{
@EmbeddedId
private ParentId id;
}
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class ParentId implements Serializable{
@Column(name = "PARENT_ID1")
private String id1;
@Column(name = "PARENT_ID2")
private String id2;
// equals, hashCode
}
(사용하는 쪽에서 @EmbeddedId로 사용하므로 @Id를 사용할 필요없고, 복합키이므로 자동생성을 사용할 수 없다)
@IdClass 처럼 정보 제공 용도로 사용하지 않고 직접 엔티티에서 사용해버렸다.
매핑 정보도 ParentId 클래스에 들어감으로써 키를 명확히 하나의 클래스로 분리한 느낌이 난다.
확실히 좀 더 객체지향적인 방법이다.
@EmbeddedId를 사용한 식별자 클래스는 아래 조건을 만족해야 한다.
@Embeddable 어노테이션을 붙여주어야 함
Serializable 인터페이스 구현해야 함
equals, hashCode 구현해야함
기본 생성자 필요
식별자 클래스는 public 이어야 함
실제 사용은 아래와 같다.
// save
public void save(){
Parent parent = Parent.builder()
.id(new ParentId("id1", "id2"))
.build();
em.persist(parent);
}
// select
public void select()}{
ParentId parentId = new ParentId("id1", "id2");
Parent foundParent = em.find(Parent.class, parentId);
}
복합키의 equals, hashCode
위의 복합키 조건을 보면 equals와 hashCode를 필수로 구현해줘야 한다고 하는데,
이는 JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용하고,
식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 하기 때문이다.
이게 단일 식별자일 경우에는 자바의 기본 타입을 사용하므로 별 문제없이 동등성이 보장되지만,
복합 식별자일 경우에는 클래스를 사용하므로 equals와 hashCode를 구현해주지 않으면 동등성을 보장할 수 없다.
ParentId id1 = new ParentId("id1", "id2");
ParentId id2 = new Parentid("id1", "id2");
assertTrue(id1.equas(id2)); // fail
같은 id 값을 가졌지만, 동등하지 않은 것이 된다.
java는 equals, hashCode를 오버라이드 하지 않으면 기본적으로 Object의 것을 사용하기 때문이다.
기본적으로 Object의 equals는 동일성 비교(==)를 하기 때문에 위의 두 키는 동등하지 않은 것이 된다.
JPA는 엔티티의 식별자를 가지고 영속성 컨텍스트를 관리하기 때문에
식별자의 동등성이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 심각한 문제가 발생할 수 있다.
그러므로 equals와 hashCode는 필수로 구현해줘야 한다.
@Entity
public class Parent{
@Id
private String parentId;
}
@Entity
@IdClass(ChildId.class)
public class Child{
// 매핑 정보 나열
@Id
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
@Id
private String childId;
}
@EqualsAndHashCode
public class ChildId implements Serializable{
private String parent; // Child.parent 에 대한 정보 제공
private String childId; // Child.childId 에 대한 정보 제공
}
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id
@ManyToOne
@JoinColumns({
@JoinColumn(name = "parent_id"),
@JoinColumn(name = "child_id")
})
private Child child;
@Id
private String grandChildId;
}
@EqualsAndHashCode
public class GrandChildId implements Serializable {
private ChildId child; // GrandChild.child 에 대한 정보 제공
private String grandChildId; // GrandChild.grandChildId 에 대한 정보 제공
}
@IdClass가 pk에 매핑되는 애들에게 정보를 바로 제공하고 있다.
@EmbeddedId
@Entity
public class Parent {
@Id
private String parentId;
}
@Entity
public class Child {
@EmbeddedId
private ChildId childId;
@MapsId("parentId")
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
@EqualsAndHashCode
@Embeddable
public class ChildId implements Serializable {
private String parentId; // @MapsId("paretnId") 로 매핑
private String childId;
}
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId grandChildId;
@MapsId("childId")
@ManyToOne
@JoinColumns({
@JoinColumn(name = "parent_id"),
@JoinColumn(name = "child_id")
})
private Child child;
}
@EqualsAndHashCode
@Embeddable
public class GrandChildId implements Serializable {
private ChildId childId; // @MapsId("childId") 로 매핑
private String grandChildId;
}
id들을 따로 묶고 @MapsId를 통해 연관관계와 id를 연결했다.
(@IdClass에서 id들을 class로 모으는 과정이 추가된 형태라고 봐도 될듯하다.)
@Entity
public class Parent {
@Id
private String parentId;
}
@Entity
public class Child {
@EmbeddedId
private ChildId childId;
}
@EqualsAndHashCode
@Embeddable
public class ChildId implements Serializable {
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
private String childId;
}
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId grandChildId;
}
@EqualsAndHashCode
@Embeddable
public class GrandChildId implements Serializable {
@ManyToOne
@JoinColumns({
@JoinColumn(name = "parent_id"),
@JoinColumn(name = "child_id")
})
private Child child;
private String grandChildId;
}
얼핏보면 more 객체지향스럽긴 하지만,
연관관계를 항상 id를 통해 접근하는 이상한 방식이 탄생하게 되고,
@Embeddable 에서 연관관계까지 equals, hashCode의 대상이 되는 이상한 구조가 탄생한다.
일대일 식별 관계(feat.@MapsId)
일대일 식별 관계는 자식 테이블의 기본키 값으로 부모 테이블의 기본키 값을 사용하는 조금 특별한 관계이다.
이 경우 연관관계의 주인이 될 외래키 칼럼이 없으므로 @MapsId를 사용하여 매핑해줘야 한다.
@Entity
public class Board{
@Id
private Long boardId;
private String title;
@OneToOne(mappedBy = "board")
private BoardDetail boardDetail;
}
@Entity
public class BoardDetail{
@Id
private Long boardId;
@Lob
private String content;
@MapsId("boardId")
@OneToOne
@JoinColumn(name = "board_id")
private Board board;
}
비식별 관계 매핑
비식별 관계는 복합키를 사용하지 않기 때문에 아주 심플하다.
@Entity
public class Parent {
@Id
private String parentId;
}
@Entity
public class Child {
@Id
private String childId;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
@Entity
public class GrandChild {
@Id
private String grandChildId;
@ManyToOne
@JoinColumn(name = "child_id")
private Child child;
}
그래서 식별이냐 비식별이냐?
데이터베이스 설계관점에서 보면, 아래와 같은 이유로 비식별 관계를 선호한다.
식별 관계는 부모 테이블의 기본키를 자식 테이블로 전파하면서 자식 테이블의 기본키 컬럼이 점점 늘어나는 구조이다.
depth가 깊어질수록 기본키 인덱스가 불필요하게 커지고, 조인할 때 SQL이 복잡해진다.
식별 관계는 2개 이상의 컬럼을 묶어서 복합 기본키를 만들어야 하는 경우가 많다.
복합 기본키는 컬럼이 하나인 단일 기본키보다 작성하는데 많은 노력이 필요하다.
식별 관계의 경우 기본키로 비즈니스 로직이 있는 자연키 컬럼을 조합하는 경우가 많고,
비식별 관계의 경우 기본키로 비즈니스와 전혀 관계없는 대리키를 주로 사용한다.
변하지 않는 요구사항이란 세상에 존재하지 않는다. 자연키 컬럼 조합은 나중에 변경될 가능성이 있다.
이런 상태에서 식별 관계로 구성할 경우 나중에 변경하기 매우 힘들어진다.
e.g. 주민등록번호
언급했듯이 비식별 관계의 경우 대리키를 주로 사용하는데, JPA는 @GeneratedValue 처럼 대리키를 생성하기 위한 편리한 방법을 제공한다.
그래서 정리하면!
될수있으면 비식별 관계를 사용하고
기본키는 Long 타입의 대리키를 사용히고
필수적 비식별 관계를 사용하자(optional = false)
조인 테이블
데이터베이스의 테이블의 연관관계를 설정하는 방법은 총 2가지이다.
조인 컬럼
일반적인 외래키 컬럼을 사용하여 연관관계를 관리하는 것
조인 테이블
별도의 테이블을 사용하여 연관관계를 관리하는 것
조인 테이블의 경우 테이블을 하나 추가해야 된다는 단점이 있다.(추가 조인 필요)
그러므로 기본적으로 조인 컬럼을 사용하고, 필요할 때만 조인 테이블을 사용하도록 해야한다.
조인 테이블 == 연결 테이블 == 링크 테이블
하나의 테이블이 여러 테이블과 관계를 맺을 수 있는 구조라던가,
원래 관계가 없었는데 관계가 생겼다거나(FK를 일괄 추가하기에는 너무 부담스럽),
관계 변경(update) 때문에 메인 테이블에 락이 걸리는 걸 방지하기 위해(연결 테이블만 컨트롤 하므로써 성능향상) 사용하는 등 여러 상황에서 사용될 수 있을 것이다.
일대일 조인테이블
@Entity
public class A {
@Id
private String id;
@OneToOne(optional = false)
@JoinTable(name = "a_b",
joinColumns = @JoinColumn(name = "a_id"),
inverseJoinColumns = @JoinColumn(name = "b_id"))
private B b;
}
@Entity
public class B {
@Id
private String id;
@OneToOne(mappedBy = "b") // optional
private A a;
}
생성되는 DDL은 아래와 같다.
CREATE TABLE A(
id varchar(255) not null,
primary key (id)
)
create table B (
id varchar(255) not null,
primary key (id)
)
create table a_b (
b_id varchar(255) not null,
a_id varchar(255) not null,
primary key (a_id),
FOREIGN KEY(a_id) REFERENCES A(a_id),
FOREIGN KEY(b_id) REFERENCES B(b_id)
)
alter table a_b
add constraint UK_pam4mvekk45ceoippm3ffvi2t unique (b_id)
a_id가 primary key, b_id에 unique constraints가 걸리면서 1:1 관계가 형성된다.
다대일 조인테이블
B를 다로 한다.
@Entity
public class B {
@Id
private String id;
@ManyToOne
@JoinTable(name = "a_b",
joinColumns = @JoinColumn(name = "a_id"),
inverseJoinColumns = @JoinColumn(name = "b_id"))
private A a;
}
@Entity
public class A {
@Id
private String id;
@OneToMany(mappedBy = "a") // optional
private List<B> bList;
}
CREATE TABLE A(
id varchar(255) not null,
primary key (id)
)
create table B (
id varchar(255) not null,
primary key (id)
)
CREATE TABLE a_b(
a_id varchar(255) not null,
b_id varchar(255) not null,
PRIMARY KEY(b_id),
FOREIGN KEY(a_id) REFERENCES A(a_id),
FOREIGN KEY(b_id) REFERENCES B(b_id)
)
다 쪽이 primary key로 생성됨으로써 다대일 관계 형성이 가능하다.
일대다 조인테이블
일대다 조인컬럼처럼 일쪽에서 연관관계를 컨트롤 하고 싶을 경우 형성하는 방법이다.
@Entity
public class A {
@Id
private String id;
@OneToMany
@JoinTable(name = "a_b",
joinColumns = @JoinColumn(name = "a_id"),
inverseJoinColumns = @JoinColumn(name = "b_id"))
private List<B> bList;
}
@Entity
public class B {
@Id
private String id;
}
일대다 조인컬럼때와 같이 단방향만을 지원한다.
아래는 생성되는 DDL이다.
CREATE TABLE A(
id varchar(255) not null,
primary key (id)
)
create table B (
id varchar(255) not null,
primary key (id)
)
CREATE TABLE a_b(
a_id varchar(255) not null,
b_id varchar(255) not null,
FOREIGN KEY(a_id) REFERENCES A(a_id),
FOREIGN KEY(b_id) REFERENCES B(b_id)
)
alter table a_b
add constraint UK_pam4mvekk45ceoippm3ffvi2t unique (b_id)
pk 대신 unique로 생성되는게 조금 다르다.
다대다 조인테이블
앞서 나왔으므로 작성하지 않겠다.
parent_id, child_id 에 각각 FK가 생성되고, PK로 묶이지는 않는다.
상속 관계 매핑(DTYPE)
RDB는 객체지향 언어처럼 상속이라는 개념이 없다.
대신
슈퍼타입 서브타입 관계
라는 모델링 기법이 있는데, 이게 상속 개념과 가장 유사하다.아래는 슈퍼타입 서브타입 관계의 전략들이다.
각각의 테이블로 변환(조인 전략)
엔티티 각각(자식, 부모 전부)을 테이블로 만들고,
자식 테이블이 부모의 기본키를 받아서
기본키 + 외래키
로 사용하는 방법이다.가장 객체지향에 가깝게 생기긴 했다.
장점
단점
DTYPE을 지정한 것은 탐색의 범위를 줄위기 위함이다(아마도?)
(부모 테이블 전체를 탐색하는 것은 낭비이기 때문이다)
DTYPE은 데이터베이스 관점에서 무조건적으로 필요한 컬럼이다.
album -> item 으로 접근할때는 상관없지만, item 에서 접근할때는 DTYPE이 없으면 어느 테이블에 조인할 지 전혀 모르기 때문이다.
DTYPE을 통해서 if문을 넣을수도 있고, 뷰모델에서 뿌리는 걸 나눠줄수도 있다.
JPA 표준 명세에서는 이를 지정해주도록 하지만 하이버네이트는 굳이 지정해주지 않아도 잘 동작한다.
사용하는 부분은 별로 다를 것 없다.
조인 전략
을 사용할 것이라는 의미이다.실행결과
hibernate에서는 조건절에 따로 DTYPE이 추가되지 않았다
통합 테이블로 변환(단일 테이블 전략)
전략 이름 그대로 하나의 테이블에 다 때려넣는 전략이다.
그리고 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다.
조인 전략과 달리 자식 -> 부모 쪽으로 접근할 수 있는 방법이 없으므로
구분 컬럼은 필수
로 사용해야한다.장점
단점
단일 테이블 전략
을 사용할 것이라는 의미이다.실행 결과
서브타입 테이블로 변환(구현 클래스마다 테이블 전략)
자식 엔티티마다 테이블을 만드는 전략이다.
자식 테이블에 필요한 컬럼이 모두 있다. 구분 컬럼이 필요없다.
장점
단점
데이터베이스 설계자와 객체지향 설계자 둘 다 추천하지 않는 방법이다.
조인이나 단일 테이블 전략을 고려하는 것이 좋다.
구현 클래스마다 테이블 전략
을 사용하겠다는 의미이다. DTYPE이 필요없다.매핑 정보만 상속(@MappedSupperClass)
부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게
매핑 정보만 제공하고 싶을 경우
사용한다.단순히 매핑 정보만 상속할 목적으로 사용한다.
BaseEntity
는 테이블과 매핑되지 않고 단순히 자식 엔티티에게 매핑 정보만 제공하는 용도로 사용된다.(ORM에서 말하는 진정한 상속 매핑은 처음 설명했던
상속 관계 매핑
을 말한다)@AttributeOverrides
를 사용한다.@AssociationOverride
를 사용한다.근데 이것보다 그냥
@Embeddable
을 쓰는게 나을 것 같다.@MappedSuperClass
는 추상 클래스만이 가능한데, 다중 상속이 안되는 자바에서 단순히 매핑 정보를 추가 정의하기 위해 상속을 써버리는 것은 좋지 않은 것 같다..일단 위에는 createdDate, lastModifiedDate로 작성했지만, 이렇게 사용하는게 좋은 예시는 아닌 것 같다.
복합키 매핑
JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.
그냥 자바 기본 타입 2개 쓰고 @Id 선언하면 안된다.
JPA에서 별도의 식별자 클래스를 만드는 방법은 2가지가 있다.
두 방식의 장단점이 있으니, 원하는 방식을 선택해서 일관성 있게 하나만 사용하는 것이 좋다.
@IdClass
@IdClass를 이용한 복합키 선언은 아래와 같다.
@IdClass가 정보 제공용도(식별자 정보는 여기를 참고해라) 정도로 쓰이고 있다.
@IdClass로 사용된 식별자 클래스는 아래 조건을 만족해야 한다.
실제 사용은 아래와 같다.
저장 코드에 식별자 클래스인 ParentId가 보이지 않는 이유는 em.persist를 호출하면 JPA가 내부에서 Parent.id1, Parent.id2 값을 이용해서 ParentId를 생성하고 영속성 컨텍스트의 키로 사용하기 때문이다.
이 부분만 보면 확실히 RDB에 가까운 방법이긴 하다.
하지만 조회쪽을 보면 또 그렇지도 않음.. 결국 ParentId를 생성해서 조회하고 있다.
@EmbededId
@IdClass보다 좀 더 객체지향적인 방법이다.
(사용하는 쪽에서 @EmbeddedId로 사용하므로 @Id를 사용할 필요없고, 복합키이므로 자동생성을 사용할 수 없다)
@IdClass 처럼 정보 제공 용도로 사용하지 않고 직접 엔티티에서 사용해버렸다.
매핑 정보도 ParentId 클래스에 들어감으로써 키를 명확히 하나의 클래스로 분리한 느낌이 난다.
확실히 좀 더 객체지향적인 방법이다.
@EmbeddedId를 사용한 식별자 클래스는 아래 조건을 만족해야 한다.
실제 사용은 아래와 같다.
복합키의 equals, hashCode
위의 복합키 조건을 보면 equals와 hashCode를 필수로 구현해줘야 한다고 하는데,
이는 JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용하고,
식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 하기 때문이다.
이게 단일 식별자일 경우에는 자바의 기본 타입을 사용하므로 별 문제없이 동등성이 보장되지만,
복합 식별자일 경우에는 클래스를 사용하므로 equals와 hashCode를 구현해주지 않으면 동등성을 보장할 수 없다.
같은 id 값을 가졌지만, 동등하지 않은 것이 된다.
java는 equals, hashCode를 오버라이드 하지 않으면 기본적으로 Object의 것을 사용하기 때문이다.
기본적으로 Object의 equals는 동일성 비교(==)를 하기 때문에 위의 두 키는 동등하지 않은 것이 된다.
JPA는 엔티티의 식별자를 가지고 영속성 컨텍스트를 관리하기 때문에
식별자의 동등성이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 심각한 문제가 발생할 수 있다.
그러므로 equals와 hashCode는 필수로 구현해줘야 한다.
식별/비식별 관계에서 복합키 사용
식별 관계와 비식별 관계
식별관계
부모 테이블의 기본키를 내려받아서 자식 테이블의 기본키 + 외래키로 사용하는 관계이다.
비식별 관계
부모 테이블의 기본키를 내려받아서 자식 테이블의 외래키로만 사용하는 관계이다.
요즘은 비식별 관계를 주로 사용하고, 필요할 때만 식별 관계를 사용하는 추세이다.
식별 관계 매핑
부모, 자식, 손자까지 계속 기본키를 전달하는 식별관계이다.
식별관계는 부모의 키를 포함해 복합키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해야 한다.
@IdClass
@IdClass가 pk에 매핑되는 애들에게 정보를 바로 제공하고 있다.
@EmbeddedId
id들을 따로 묶고 @MapsId를 통해 연관관계와 id를 연결했다.
(@IdClass에서 id들을 class로 모으는 과정이 추가된 형태라고 봐도 될듯하다.)
※ 번외로 아래와 같이 세팅 할수도 있는데, 이는 잘못된 방식이다.
얼핏보면 more 객체지향스럽긴 하지만,
연관관계를 항상 id를 통해 접근하는 이상한 방식이 탄생하게 되고,
@Embeddable 에서 연관관계까지 equals, hashCode의 대상이 되는 이상한 구조가 탄생한다.
일대일 식별 관계(feat.@MapsId)
일대일 식별 관계는 자식 테이블의 기본키 값으로 부모 테이블의 기본키 값을 사용하는 조금 특별한 관계이다.
이 경우 연관관계의 주인이 될 외래키 칼럼이 없으므로 @MapsId를 사용하여 매핑해줘야 한다.
비식별 관계 매핑
비식별 관계는 복합키를 사용하지 않기 때문에 아주 심플하다.
그래서 식별이냐 비식별이냐?
데이터베이스 설계관점에서 보면, 아래와 같은 이유로 비식별 관계를 선호한다.
depth가 깊어질수록 기본키 인덱스가 불필요하게 커지고, 조인할 때 SQL이 복잡해진다.
복합 기본키는 컬럼이 하나인 단일 기본키보다 작성하는데 많은 노력이 필요하다.
비식별 관계의 경우 기본키로 비즈니스와 전혀 관계없는 대리키를 주로 사용한다.
변하지 않는 요구사항이란 세상에 존재하지 않는다. 자연키 컬럼 조합은 나중에 변경될 가능성이 있다.
이런 상태에서 식별 관계로 구성할 경우 나중에 변경하기 매우 힘들어진다.
그래서 정리하면!
optional = false
)조인 테이블
데이터베이스의 테이블의 연관관계를 설정하는 방법은 총 2가지이다.
조인 테이블의 경우 테이블을 하나 추가해야 된다는 단점이 있다.(추가 조인 필요)
그러므로 기본적으로 조인 컬럼을 사용하고, 필요할 때만 조인 테이블을 사용하도록 해야한다.
하나의 테이블이 여러 테이블과 관계를 맺을 수 있는 구조라던가,
원래 관계가 없었는데 관계가 생겼다거나(FK를 일괄 추가하기에는 너무 부담스럽),
관계 변경(update) 때문에 메인 테이블에 락이 걸리는 걸 방지하기 위해(연결 테이블만 컨트롤 하므로써 성능향상) 사용하는 등 여러 상황에서 사용될 수 있을 것이다.
일대일 조인테이블
생성되는 DDL은 아래와 같다.
a_id가 primary key, b_id에 unique constraints가 걸리면서 1:1 관계가 형성된다.
다대일 조인테이블
B를
다
로 한다.다 쪽이 primary key로 생성됨으로써 다대일 관계 형성이 가능하다.
일대다 조인테이블
일대다 조인컬럼처럼
일
쪽에서 연관관계를 컨트롤 하고 싶을 경우 형성하는 방법이다.일대다 조인컬럼때와 같이 단방향만을 지원한다.
아래는 생성되는 DDL이다.
pk 대신 unique로 생성되는게 조금 다르다.
다대다 조인테이블
앞서 나왔으므로 작성하지 않겠다.
parent_id, child_id 에 각각 FK가 생성되고, PK로 묶이지는 않는다.
엔티티 하나에 여러 테이블 매핑
아까 위의 일대일 식별 관계에서 나왔었던 형태이다.
자주 사용하는 형태는 아니지만 가끔 나오기도 한다.
@SecondaryTable을 사용해 board_detail 테이블을 추가로 매핑했다.
테이블을 2개로 나누는건 의미가 있어서 2개로 나눈건데, 이렇게 엔티티 하나로 묶으면 항상 같이 조회될것이다.
그럼 무슨 의미가 있는건지..ㅋㅋ 어디서 쓰일지 궁금하다.