이번시간에는 JPA 고급 매핑을 알아보겠다.
실무에서 잘 쓰이지 않는 개념도 있으니, 필요한 것만 알아가도 좋을 것 같다.
관계형 데이터베이스에는 상속이라는 개념과 유사한 슈퍼타입 서브타입 관계 라는 모델링 기법이 있다.
이 슈퍼타입 서브타입 논리 모델을 물리 모델인 테이블로 구현할 땐 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;
}
@Entity
@DiscriminatorValue("A")
@PrimaryKeyJoinColumn (name = "FOOD_ID") //(4)
public class Food extends Item{
private String chef;
}
//... Movie, Book 도 매핑...
(1) @Inheritance(strategy = InheritanceType.JOINED)
: 상속 매핑은 부모 클래스에 @Inheritance 를 사용 해야 하고, 매핑 전략을 지정해야 함. 여기서는 조인 전략을 사용하므로 InheritanceType.JOINED 를 사용함.
(2) @DiscriminatorColumn(name ="DTYPE")
: 부모 클래스에 구분 컬럼을 지정하는 어노테이션. 기본값이 DTYPE 이므로 여기서는 @DiscriminatorColumn 으로 줄여 사용해도 무관함.
(3) @DiscriminatorValue("A")
: 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정함. 여기서 푸드 엔티티를 저장하면 DTYPE에 A 가 저장됨.
(4) @PrimaryKeyJoinColumn
: 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 이 어노테이션을 이용하면 됨.
✅ 조인 전략의 장단점
▶ 단일 테이블 전략 (통합 테이블로 변환)
단일 테이블 전략은 위의 테이블과 같이 말 그대로, 하나의 테이블에 모든 값을 담아 사용하는 전략이다. 그리고 구분 컬럼 (여기서는 DTYPE) 으로 어떤 자식 테이터가 저장되었는지 구분한다.
이렇게 하면 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.
이 전략을 사용할 땐 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해 주어야 한다.
Food 엔티티를 저장하면, 다른 엔티티와 매핑된 director, actor, author은 null 값이 입력되기 때문이다.
코드로 살펴보겠다.
@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;
}
@Entity
@DiscriminatorValue("A")
@PrimaryKeyJoinColumn (name = "FOOD_ID")
public class Food extends Item{
private String chef;
}
// Movie, Book 도 mapping... //
코드 상으로는 전략을 InheritanceType.SINGLE_TABLE 로 지정한 것 말고는
앞선 코드와 변화한 점은 없다.
✅ 단일 테이블 전략 장단점
+) @DiscrmininatorValue를 지정하지 않으면 기본적으로 엔티티 이름을 사용한다.
▶ 구현 클래스마다 테이블 전략 (서브타입 테이블로 변환)
위의 그림과 같이, 자식 엔티티마다 테이블을 만들고, 자식 테이블 각각에 필요한 컬럼을 모두 넣는 전략이다.
코드로 알아보겠다.
@Entity
@Inheritance (strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id @GeneratedValue
@Column (name = "ITEM_ID")
private long id;
private String name;
}
@Entity
public class Food extends Item{
private String chef;
}
// Movie, Book 도 mapping... //
이전 전략들과 가장 큰 차이점은 구분 컬럼을 사용하지 않는 것이다.
당연히, 필요한 모든 컬럼들이 있는 테이블을 자식 엔티티 마다 생성해주니 굳이 구분 컬럼이 필요하지 않다.
✅ 구현 클래스마다 테이블 전략의 장단점
DB 설계자와 ORM 전문가 모두 추천하지 않는 전략이다. 가급적 사용하지 말자.
▶ + @MappedSuperClass
부모 클래스는 테이블과 매핑하지 않고, 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperClass 어노테이션을 사용하면 된다.
흔히 엔티티마다 있는 등록일자, 수정일자, 등록자, 수정자 같은 AUDIT 컬럼과 같이 여러 엔티티에서 공통으로 사용하는 속성을 관리하고자 할 때 유용하다.
코드로 살펴보자.
Member 와 Seller에 공통으로 id 와 name 컬럼이 있다고 가정했을 때, 테이블은 그대로 두고, 이 두 공통 속성을 부모 클래스로 모으고 객체 상속 관계로 만들어 보겠다.
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue
private long id;
private String name;
}
@Entity
public class Member extends BaseEntity {
private String email;
// ID와 NAME은 상속받기 때문에
// 자식 엔티티에서 매핑할 이유가 없다
}
//Seller 도 Member 와 유사하게 매핑시켜 준다...
부모로부터 물려받은 매핑 정보를 재정의 하려면 @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 (...)
// 부모의 id 속성의 컬럼명을 MEMBER_ID 로,
// 부모의 name 속성의 컬럼명을 MEMBER_ID 로 재정의 하였다.
@MappedSupperclass로 지정한 클래스는 엔티티가 아니므로, 당연히 em.find() 나 JPQL에서 사용할 수 없다.
그리고 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것이 권장된다.
정리하자면 테이블에 매핑되지 않고, 여러 엔티티에서 사용하는 공통 컬럼들을 관리하고자 할 때 사용되는 어노테이션이다.
용어 알고 가기
식별 관계 : 부모 테이블의 기본 키를 내려 받아 자식 테이블에서 기본 키 + 외래 키로 사용하는 관계
비식별 관계 : 부모 테이블의 기본 키를 자식 테이블에서 외래 키로만 사용하는 관계
필수적 비식별 관계 : 외래 키에 NULL 허용 X
선택적 비식별 관계 : 외래 키에 NULL 허용 0
▶ 복합 키 : 비식별 관계 매핑
앞선 JPA 포스팅에서 말했듯 복합 키를 사용하려는 경우 별도의 식별자 클래스를 만들고 , 그곳에 equals 와 hashCode를 구현해야 한다.
JPA에서는 복합 키를 지원하기 위해 @IdClass 와 @EmbeddedId 2가지 방법을 제공하고 있다.
여기서 @IdClass 는 관계형 데이터베이스에 가깝고, @EmbeddedId는 좀 더 객체지향에 가까운 방법이다.
위의 관계를 먼저 @IdClass 로 표현 해 보겠다.
//식별자 클래스
public class ParentId implements Serializable {
private String id1; //Parent.id1 에 매핑 될 것임
private String id2; //Parent.id2 에 매핑 될 것임
//equals, hashCode override 하기
}
//부모 클래스
@Entity
@IdClass (ParentId.class ) //앞서 만든 식별자 클래스를 넣어줌
publi class Parent {
@Id
@Column (name = "PARENT_ID1")
private String id1; //ParentId.id1과 연결. 둘의 필드 이름이 같아야 함.
@Id
@Column (name = "PARENT_ID2")
private String id2; //ParentId.id2와 연결. 마찬가지로 둘의 필드 이름이 같아야 함.
private Strng name;
}
//사용 예시 (1) - 저장
Parent parent = new Parent();
parent.setId1("id1");
parent.setId2("id2");
parent.setName("name");
entityManager.persist(parent);
//영속성 컨텍스트에 등록하기 직전에, 내부에서 Parent.id1, Parent.id2의 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용함.
//사용 예시 (2) - 조회
ParentId = parentId = new ParentId("id1","id2");
Parent parent = entityManager.find(Parent.class , parentId);
//식별자 클래스인 ParentId를 사용해서 엔티티를 조회하였음.
//자식 클래스
@ntity
public class Child {
@Id
private String id;
@ManyToOne
@JoinColums( {
@JoinColumn (name = PARENT_ID1",
referencedColumnName = "PARENT_ID1"),
@JoinColumn (name = "PARENT_ID2",
referencedColumnName = "PARENT_ID2")
} )
private Parent parent;
//자식 테이블의 복합 외래 키를 위와 같이 표현함.
// 각각의 외래 키 컬럼을 @JoinColumn으로 매핑하고,
// 위의 예시처럼 @JoinColumn 의 name 속성과
// referencedColumnName 속성의 값이 같으면 referencedColumnName은 생략해도 무방함.
이제 @EmbeddedId로 표현 해 보겠다.
//식별자 클래스
@Embeddable //이 어노테이션을 붙여줘야 함
public class ParentId implements Serializable {
@Column (name = "PARENT_ID1")
private String id;
@Column (name = "PARENT_ID2")
private String id2;
//equals , hashCode overring 구현...
}
//부모 클래스
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
priavte String name;
}
//사용 예시 1 - 저장
Parent parent = new Parent();
ParentId parentId = new ParentId("id1","id2");
parent.setId(parentId);
parent.setName("name");
entityManage.persist(parent);
//이번에는 직접 parentId를 생성해서 사용한다.
//사용 예시 2 - 조회
ParentId parentId = new ParentId("id1","id2");
Parent parent = entityManger.find(Parent.class, parentId);
//조회도 parentId를 직접 사용해서 조회한다.
👊 IdClass vs EmbeddedId
각각의 장단점이 있어 취향껏 선택하면 되는데,
특정 상황에서 @EmbeddedId가 JPQL이 조금 더 길어질 수도 있다.
(ex:
"select p.id.id1, p.id.id2 from Parent p " //embeddedId
"select p.id1, p.id2 from Parent p " / idClass
▶ 복합 키 : 식별 관계 매핑
부모 , 자식 , 손자까지 계속 기본 키를 전달하는 식별 관계를 표현 해 보자.
@IdClass
//부모
@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")
public Parent parent;
@Id @Column (name = "CHILD_ID")
private String childId;
private String name;
}
//자식 ID
public class ChildId implements Serializable {
private String parent;
private String 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 = "GRANDHCILD_ID")
private String id;
priavet String name;
}
//손자 ID 클래스 구현... 앞선 Child 와 유사하게 구현하면 됨...
식별관계에서는 기본 키와 외래 키를 같이 매핑해야 한다.
따라서 @Id 와 @ManyToOne을 같이 사용하면 된다.
@EmbeddedId와 식별 관계
@EmbeddedId 로 식별 관계를 나타낼 때는 @MapsId를 사용해야 한다.
//부모 엔티티는 위와 동일하다.
//자식
@Entity
public class Child {
@EmbeddedId
private ChildId id;
@MapsId("parentId") //ChildId.parentId에 매핑
@ManyToOne
@JoinColumn (name = "PARENT_ID")
public Parent parent;
priavet String name;
}
//자식 ID
@Embeddable
public class ChildId implements Serializable {
private String parentId; //@MapsId("parentId")로 매핑
@Column (name = "CHILD_ID")
private String id;
//equals, hashCode overriding
}
코드에서 볼 수 있듯이,
@MapsId는 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻이다.
▶ 비식별 관계로 구현
비식별 관계로 구현하면, 앞선 포스팅에서 보았듯이
기본키나 외래 키가 복합키로 구성되지 않기에 매핑도 쉽고 코드도 단순해진다.
그리고 무엇보다 따로 복합 키 클래스를 만들지 않아도 된다.
//자식
@Entity
public class Child {
@Id @GeneratedValue
@Column (name = "CHILD_ID")
private Long id;
@ManyToOne
@JoinColumn (name = "PARENT_ID")
private Parent parent;
}
//손자
@Entity
public class GrandChild {
@Id @GeneratedValue
@Column (name = "GRANDCHILD_ID")
private Long id;
@ManyToOne
@JoinColumn (name = "CHILD_ID")
private Child child;
}
▶ 일대일 식별 관계
일대일 식별 관계의 경우, 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용하게 된다. 따라서 부모 테이블의 기본 키가 복합 키가 아니라면 자식 테이블의 기본 키는 복합 키로 구성하지 않아도 된다.
이 경우 연관관계이 주인 테이블에서 @MapsId를 사용하고 속성 값은 비워두면 된다.
식별, 비식별 관계에서 보통 비식별 관계가 선호됨.
식별관계의 경우 부모 테이블 기본 키를 자식에게 전파하면서, 자식 테이블의 기본 키 컬럼이 점점 늘어나게 됨. -> 조인 시 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있음.
다만 식별 관계는 기본 키 인덱스를 활요하기 좋고, 특정 상황에서 조인 없이 하위 테이블만으로도 검색을 완료할 수 있기에 상황에 따라 식별 관계에서도 큰 이점을 얻을 수 있음.
그 동안 조인 컬럼을 사용 (외래 키)하여 연관관계를 설계 해 보았다.
이번에는 조인 테이블을 사용해서 연관관계를 설계하겠다.
조인 테이블을 사용하려면, 연관 관계를 관리하는 조인 테이블을 추가로 생성하고, 여기에 두 테이블의 외래 키를 가지고 연관관계를 관리해야 한다.
이 전략의 가장 큰 단점은 테이블을 하나 더 생성해야 한다는 점이다.
따라서 기본적으로는 조인 컬럼 전략을 사용하고 필요하다고 생각되면 조인 테이블을 사용하는 것이 좋다.
▶ 일대일 조인 테이블
//parent
@Entity
public class Parent {
@Id @GeneratedValue
@Colun (name = "PARENT_ID")
private long id;
private String name;
@OneToOne
@JoinTable (name = "PARENT_CHILD",
joinColumns = @JoinColumn (name = "PARENT_ID"),
inverseJoinColumns = @JoinColumn (name = "CHILD_ID")
private Child child;
}
//Child
public class Child {
@Id @GeneratedValue
@Column (name = "CHILD_ID")
private Long id;
priave String name;
}
@JoinColumn 대신에 @JoinTalbe을 사용한 것을 알 수 있다.
일대다, 다대일 조인 테이블은 앞선 포스팅을 다 읽었다면 무리 없이 구현할 수 있을 것이다.
테이블 관계만 짚어보자면,
일대다 관계에서는 조인 테이블의 컬럼 중 다와 관련된 컬럼에 유니크 제약조건을 걸어야 한다.
다대다 관계에서는 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니트 제약조건을 걸어야 한다.
여기서 예측가능하듯이 조인 테이블에 컬럼을 추가하면 @JoinTable 전략을 사용할 수 없다. 이 경우 새로운 엔티티를 만들어서 조인 테이블과 매핑해야 한다.
잘 사용하지는 않는 방법이지만 @SecondaryTable을 사용해서 한 엔티티에 여러 테이블을 매핑할 수 있다.
@Entity
@Table (name ="BOARD")
@SecondaryTable (name = "BOARD_DETAIL",
pkJoinColumns = @PrimaryKeyJoinColumn (name = "BOARD_DETAIL_ID"))
public class Board {
@Id
@Column (name = "BOARD_ID"
private Long id;
private String title;
@Column (talbe = "BOARD_DETAIL")
private String content;
}
@SecondaryTable 을 사용해서 BOARD_DETAIL 테이블과 추가로 매핑이 되었다.
여기서 content 필드는 table = "BOARD_DETAIL"을 사용해서 BOARD_TALBLE 의 컬럼과 매핑이 되었다.
title처럼 테이블을 지정하지 않으면 기본 테이블 (=BOARD)에 매핑된다.
이 방법은 항상 두 테이블을 조회하므로 최적화하기가 어렵다.
그래서 테이블 당 엔티티를 가각 만들어서 일대일 매핑하는 것이 권장된다.
다음 시간에는 지연 로딩과 프록시에 대해 알아보겠다.
이번 시간에는 코드가 많았는데, 다시 말하지만 필요한 부분만 살펴보고 넘어가도 좋을 것 같다.
참조 : 자바 ORM 표준 JPA 프로그래밍 : 김영한