[jpa] 고급 매핑

상속 관계 매핑

RDB는 객체지향 언어처럼 상속이라는 개념이 없다.
대신 슈퍼타입 서브타입 관계라는 모델링 기법이 있는데, 이게 상속 개념과 가장 유사하다.
상속 관계 매핑 기법은
물리 모델로 구현된 어떠한 슈퍼타입 서브타입 관계든, 객체 지향 상속 기법으로 추상화해서 접근할 수 있게 해준다는 것이 핵심이다.
DB를 슈퍼타입 서브타입 관계의 조인 전략으로 모델링했든, 단일 테이블 전략으로 모델링했든 객체 입장에서는 동일한 상속 구조로 접근할 수 있다.

각각의 테이블로 변환(조인 전략)

엔티티 각각(자식, 부모 전부)을 테이블로 만들고, 자식 테이블이 부모의 기본키를 받아서 기본키 + 외래키로 사용하는 방법이다.

joined tale

클래스 상속 구조랑 가깝게 생기긴 했다.

  • 장점

    • 테이블이 정규화된다
    • 외래키 참조 무결성 제약 조건 사용 가능
    • 저장 공간을 효율적으로 사용함(불필요하게 null을 넣는 부분이 없으므로)
  • 단점

    • 조회할 때 항상 조인해서 들고와야함
    • 등록할 때 INSERT를 항상 2번 실행해야함

초반에 모든 것이 예측된 케이스가 아니라면 이 전략을 사용할 일이 별로 없을 것 같다.
객체는 리팩토링을 통해서 공통 부분을 추출해내는 등 자유롭게 변경될 수 있지만, 데이터베이스는 마이그레이션이 필요하기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/**
* 엔티티 정의
**/
@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(name = "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);
}

사용하는 부분은 별로 다를 것 없다.

  1. 상속 매핑을 사용할 것이고, 조인 전략을 사용할 것이라는 의미이다.
  2. 자식 테이블을 구분할 컬럼이다. 실제 테이블의 컬럼으로 생성된다. 기본값이 DTYPE 이다.

조인 전략에서는 이 컬럼이 생략 가능하다.(hibernate는 그렇고, 다른 구현체는 아닐수도 있다)
자식으로 직접 접근할때는(e.g. Album 엔티티로 접근) 생략해도 문제가 되지 않는데, 부모로 직접 접근할때는(e.g. 통계를 위해 Item 엔티티로 접근) TYPE이 없으면 해당 데이터가 어느 데이터를 나타내는지 알 수 없다.
그러므로 hibernate는 아래와 같은 전략을 선택한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
select
-- ~~~
case
-- 상속된 테이블의 개수만큼 when 반복
when itemlist0_1_.id is not null then 1
when itemlist0_2_.id is not null then 2
when itemlist0_.id is not null then 0
end as clazz_1_
from
item itemlist0_
left outer join
movie itemlist0_1_
on itemlist0_.id=itemlist0_1_.id
left outer join
album itemlist0_2_
on itemlist0_.id=itemlist0_2_.id
-- 상속된 테이블의 개수만큼 left join 반복

결국 이런식으로 처리되기 때문에, TYPE을 지정해주는 것이 좋다.

  1. 구분 컬럼에 저장될 값이다. 생략하면 엔티티 이름을 사용한다.

https://stackoverflow.com/questions/3639225/single-table-inheritance-strategy-using-enums-as-discriminator-value
@DiscriminatorValue에 enum을 사용하는 방법이라는데, 왜 이렇게 쓰는지 모르겠다.

  1. 기본적으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 이를 바꿔주고 싶을 때 사용한다.

실행결과

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 삽입
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이 추가되지 않았다)

통합 테이블로 변환(단일 테이블 전략)

전략 이름 그대로 하나의 테이블에 다 때려넣는 전략이다.
저장된 서브 타입마다 사용하지 않는 컬럼들에는 null이 들어가게 된다.

single table

조인 전략과 달리 구분 컬럼은 생략이 불가능하다.
생략하면 기본값이 사용된다.

  • 장점

    • 조인이 필요없다
  • 단점

    • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.(데이터 관점에서 아주 좋지 않음)
    • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 오히려 성능이 느려질 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@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{
}
  1. 단일 테이블 전략을 사용할 것이라는 의미이다.

실행 결과

1
2
3
4
5
6
7
8
9
10
11
-- 삽입
INSERT INTO ITEM(id, name, price, artist, DTYPE) VALUES(1, '앨범', 10000, '소녀시대', 'A');

-- 조회
select
*
from
item album0_
where
album0_.id=1
and album0_.DTYPE='A'

서브타입 테이블로 변환(구현 클래스마다 테이블 전략)

실제 데이터들을 모두 별도의 테이블에 저장하는 방법이다.
테이블이 모두 별개이다 보니, 공통 부분에 대한 내용이 보장되지도 않고, 관리하기도 힘들다.
쿼리도 전부 UNION으로 날라가서 성능 로스가 극심하다.

concrete table

데이터베이스 설계자와 객체지향 설계자 둘 다 추천하지 않는 방법이다.
조인이나 단일 테이블 전략을 고려하는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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{
}
  1. 구현 클래스마다 테이블 전략을 사용하겠다는 의미이다.

매핑 정보만 상속(@MappedSuperClass)

부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶을 경우 사용한다.
단순히 매핑 정보만 상속할 목적으로 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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에서 말하는 진정한 상속 매핑은 처음 설명했던 상속 관계 매핑을 말한다.)

  1. 매핑 정보만 제공할 클래스라는 의미이다.
  2. 매핑정보를 재정의 하고 싶을 경우 사용한다. 여러개를 지정하고 싶을 경우 @AttributeOverrides를 사용한다.
  3. 위에 명시하진 않았지만 관계를 재정의 하고 싶을 경우 @AssociationOverride를 사용한다.

근데 이것보다 그냥 @Embeddable을 쓰는게 나을 것 같다.
@MappedSuperClass는 추상 클래스만이 가능한데, 다중 상속이 안되는 자바에서 단순히 매핑 정보를 추가 정의하기 위해 상속을 써버리는 것은 좋지 않은 것 같다…
일단 위에는 createdDate, lastModifiedDate로 작성했지만, 이렇게 사용하는게 좋은 예시는 아닌 것 같다.

복합키 매핑

JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.
그냥 자바 기본 타입 2개 쓰고 @Id 선언하면 안된다.

JPA에서 별도의 식별자 클래스를 만드는 방법은 2가지가 있다.
두 방식의 장단점이 있으니, 원하는 방식을 선택해서 일관성 있게 하나만 사용하는 것이 좋다.

@IdClass

@IdClass를 이용한 복합키 선언은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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 이어야 함

실제 사용은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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);
}

@IdClass의 장점은 엔티티에 flat한 attribute를 제공해서 그나마 RDB와 가깝다는 것인데, 저장만 그렇지 조회는 또 그렇지도 않다.
(저장의 경우 em.persist를 호출하면 JPA가 내부에서 Parent.id1, Parent.id2 값을 이용해서 ParentId를 생성하고 영속성 컨텍스트의 키로 사용한다.)

@EmbededId

@IdClass보다 좀 더 객체지향적인 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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 클래스에 들어감으로써 키를 명확히 하나의 클래스로 분리한 느낌이난다. 좀 더 객체지향적인 방법이다.(id1에 접근하고자 할 경우 entity.getId().getId1()처럼, 언뜻보기에 좀 이상한 접근법이 사용되긴 하지만)

@EmbeddedId를 사용한 식별자 클래스는 아래 조건을 만족해야 한다.

  • @Embeddable 어노테이션을 붙여주어야 함
  • Serializable 인터페이스 구현해야 함
  • equals, hashCode 구현해야함
  • 기본 생성자 필요
  • 식별자 클래스는 public 이어야 함

실제 사용은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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);
}

의문 : @EmbeddedId 사용시 JPQL에서 id.id1 의 형태로 접근해야하는데, delegate 메서드를 활용할 순 없나?
결과 : 엔티티의 대상이 필드이기 때문에 delegate 메서드로는 불가능하다 (대상이 아니기때문)

복합키의 equals, hashCode

위의 복합키 조건을 보면 equals와 hashCode를 필수로 구현해줘야 한다고 하는데,
이는 JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용하고,
식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 하기 때문이다.

이게 단일 식별자일 경우에는 자바의 기본 타입을 사용하므로 별 문제없이 동등성이 보장되지만,
복합 식별자일 경우에는 클래스를 사용하므로 equals와 hashCode를 구현해주지 않으면 동등성을 보장할 수 없다.

1
2
3
4
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는 필수로 구현해줘야 한다.

식별/비식별 관계에서 복합키 사용

식별 관계와 비식별 관계

식별관계

부모 테이블의 기본키를 내려받아서 자식 테이블의 기본키 + 외래키로 사용하는 관계이다.

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE parent(
parent_id integer,
PRIMARY KEY(parent_id)
)

CREATE TABLE child(
parent_id integer,
child_id integer,
PRIMARY KEY(parent_id, child_id),
FOREIGN KEY(parent_id) REFERENCES parent(parent_id)
)

비식별 관계

부모 테이블의 기본키를 내려받아서 자식 테이블의 외래키로만 사용하는 관계이다.
요즘은 비식별 관계를 주로 사용하고, 필요할 때만 식별 관계를 사용하는 추세이다.

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE parent(
parent_id integer,
PRIMARY KEY(parent_id)
)

CREATE TABLE child(
parent_id integer,
child_id integer,
PRIMARY KEY(child_id),
FOREIGN KEY(parent_id) REFERENCES parent(parent_id)
)
  1. 필수적 비식별 관계 : FK NOT NULL(INNER JOIN 사용됨)
  2. 선택적 비식별 관계 : FK NULLALBE(OUTER JOIN 사용됨)

식별 관계 매핑

부모, 자식, 손자까지 계속 기본키를 전달하는 식별관계이다.
식별관계는 부모의 키를 포함해 복합키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE parent(
parent_id integer,
PRIMARY KEY(parent_id)
)

CREATE TABLE child(
parent_id integer,
child_id integer,
PRIMARY KEY(parent_id, child_id),
FOREIGN KEY(parent_id) REFERENCES parent(parent_id)
)

CREATE TABLE grandchild(
parent_id integer,
child_id integer,
grandchild_id integer,
PRIMARY KEY(parent_id, child_id, grandchild_id),
FOREIGN KEY(parent_id) REFERENCES parent(parent_id),
FOREIGN KEY(child_id) REFERENCES child(child_id)
)

@IdClass

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@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로 모으는 과정이 추가된 형태라고 봐도 될듯하다.)

@mapsid는 @id로 지정한 컬럼에 @OnetoOne이나 @ManyToOne 관계를 매핑시키는 역할을 한다.
http://docs.jboss.org/hibernate/jpa/2.2/api/javax/persistence/MapsId.html
매핑의 대상이 되는 속성은 @OnetoOne이나 @ManyToOne의 기본키와 타입이 같아야한다.

※ 번외로 아래와 같이 세팅 할수도 있는데, 이는 잘못된 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@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의 대상이 되는 이상한 구조가 탄생한다.
id는 id대로 놔둬야 한다.

일대일 식별 관계(feat.@MapsId)

일대일 식별 관계는 자식 테이블의 기본키 값으로 부모 테이블의 기본키 값을 사용하는 조금 특별한 관계이다.
이 경우 연관관계의 주인이 될 외래키 칼럼이 없으므로 @MapsId를 사용하여 매핑해줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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;
}

board.getBoardDetail().getContent()로 접근해야해서 사용이 조금 자연스럽지 않게 느껴지지만, @Delegate 같은것으로 충분히 해결할 수 있다.

비식별 관계 매핑

비식별 관계는 복합키를 사용하지 않기 때문에 아주 심플하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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) 때문에 메인 테이블에 락이 걸리는 걸 방지하기 위해(연결 테이블만 컨트롤 하므로써 성능향상) 사용하는 등 여러 상황에서 사용될 수 있을 것이다.

일대일 조인테이블

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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은 아래와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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를 로 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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로 생성됨으로써 다대일 관계 형성이 가능하다.

일대다 조인테이블

일대다 조인컬럼처럼 쪽에서 연관관계를 컨트롤 하고 싶을 경우 형성하는 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@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이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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로 묶이지는 않는다.

엔티티 하나에 여러 테이블 매핑

아까 위의 일대일 식별 관계에서 나왔었던 형태이다.

board와 board_detail을 나눠서 저장하고, 같은 PK를 쓰는 형태

자주 사용하는 형태는 아니지만 가끔 나오기도 한다.

1
2
3
4
5
6
7
8
9
10
11
12
@Entity
@SecondaryTable(name = "board_detail", // 1
pkJoinColumns = @PrimaryKeyJoinColumn(name = "board_detail_id")) // 2
public class Board{
@Id
private Long boardId;

private String title;

@Column(table = "board_detail") // 3
private String content;
}

@SecondaryTable을 사용해 board_detail 테이블을 추가로 매핑했다.

  1. 추가로 매핑할 테이블의 이름이다.
  2. 추가로 매핑된 테이블의 기본키 컬럼명이다.
  3. 추가로 매핑된 테이블에 저장될 속성이다.
1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE board (
board_id BIGINT NOT NULL,
title VARCHAR(255),
PRIMARY KEY (board_id)
)

CREATE TABLE board_detail (
board_detail_id BIGINT NOT NULL,
content VARCHAR(255),
PRIMARY KEY (board_detail_id)
)

위의 일대일 식별관계와 반대로, board.getContent()로 접근할 수 있어 사용은 자연스럽다.
하지만 이 방식의 경우, 매핑이 부자연스럽다.(lazy loading도 안된다)
결과적으로 사용의 자연스러움 보다는 매핑의 자연스러움을 추구하는 것이 맞는것 같다. 그니까… 이거 어디서 쓸일은 없을꺼 같음…ㅋ