티스토리 뷰
이전 글: https://gojs.tistory.com/46
JPA 연관관계 매핑 (1)
관계형 데이터베이스의 테이블간의 관계를 객체간의 연관관계로 표현할 수 있다.크게 단방향 연관관계과 양방향 연관관계로 구분할 수 있으며 이에 대해 알아보자. 단방향 연관관계회원과 팀
gojs.tistory.com
이전 글에서 JPA에서의 기본적인 연관관계 매핑하는 방법에 대해서 알아보았다.
그렇다면 실제로 충분히 마주하게 될만한 특수한 케이스들에 대해서 연관관계 매핑을 어떻게 할 수 있을지 알아보자.
상속 관계 매핑
위와 같이 상속 관계를 포함한 도메인 모델을 테이블로 구현할 때에는 크게 3가지 방법이 있다.
- 각각의 테이블로 변환
- 하나의 통합 테이블로 변환
- 서브타입 테이블로 변환
각각의 테이블로 변환
각 엔티티를 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략을 조인 전략이라고 한다.
이 경우 부모 테이블에서 어떤 자식 테이블과 매핑되는지 구분할 수 있는 타입 컬럼이 추가적으로 붙는다.
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
...
}
@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;
}
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")
public class Book extends Item {
private String author;
private String isbn;
}
위 코드에서 새로 등장한 어노테이션들은 아래와 같은 의미를 가지고 있다.
- @Inheritance(strategy = InheritanceType.JOINED)
- 부모 클래스에 선언하는 어노테이션
- 매핑 전략에 따라 속성의 값이 바뀐다
- @DiscriminatorColumn(name = “DTYPE”)
- 부모 클래스에 구분 컬럼을 지정
- @DiscriminatorValue(”M”)
- 엔티티 저장 시 구분 컬럼에 입력할 값을 지정
- @PrimaryKeyJoinColumn(name = “BOOK_ID”)
- 기본적으로 자식 테이블은 부모 테이블의 ID 컬럼명 사용
- ID 컬럼명 명시하여 사용할 필요가 있을 때 해당 어노테이션 사용
조인 전략으로 상속 관계의 도메인 모델을 구현하는 경우 아래와 같은 특징이 있다.
- 장점
- 테이블이 정규화된다.
- 외래 키 제약조건을 활용할 수 있다.
- 저장공간을 효율적으로 사용한다.
- 단점
- 조회 시 조인이 발생하여 성능 저하될 수 있다.
- 조회 쿼리가 복잡하다.
- 데이터 등록 시 insert 쿼리 두 개 발생한다.
하나의 통합 테이블로 변환
부모-자식 관계의 도메인 모델을 하나의 통합 테이블로 변환하는 전략을 단일 테이블 전략이라고 한다.
@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 int price;
...
}
@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;
}
@Entity
@DiscriminatorValue("B")
public class Book extends Item {
private String author;
private String isbn;
}
단일 테이블 전략을 사용하기 위해 @Inheritance 어노테이션의 value를 InheritanceType.SINGLE_TABLE로 설정했다.
테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수적으로 사용해야 하며 각 컬럼들은 null이 허용된다.
- 장점
- 조인이 필요 없으므로 조회 성능이 빠르다
- 조회 쿼리가 단순하다
- 단점
- 자식 엔티티가 매핑한 컬럼은 모두 nullable하다
- 테이블이 커질 수 있어서 오히려 성능이 느려지는 경우가 발생할 수 있다
서브타입 테이블로 변환
각 자식 엔티티마다 테이블을 만드는 전략을 구현 클래스마다 테이블 전략이라고 한다.
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {
@Id @GeneratedValue
@Column(name = "ITEM_ID")
private Long id;
private String name;
private int price;
...
}
@Entity
public class Album extends Item {
private String artist;
}
@Entity
public class Movie extends Item {
private String director;
private String actor;
}
@Entity
public class Book extends Item {
private String author;
private String isbn;
}
구현 클래스마다 테이블 전략을 사용하기 위해 @Inheritance 어노테이션의 value를 InheritanceType.TABLE_PER_CLASS로 설정했다.
- 장점
- 서브 타입을 구분해서 처리할 때 효과적이다
- not null 제약조건을 사용할 수 있다
- 단점
- 여러 자식 테이블을 한번에 조회할 때 성능이 느리다 (UNION)
- 자식 테이블을 통합해서 쿼리하기 어렵다
- 일반적으로 비추천
@MappedSuperclass
@MappedSuperclass 어노테이션을 부모 클래스에 선언하게되면 부모 클래스는 테이블 매핑을 하지 않고 이를 상속받는 자식 클래스만 테이블로 매핑한다.
@MappedSuperclass
public abstract class BaseEntity {
@Id @GeneratedValue
private Long id;
private String name;
}
@Entity
public class Member extends BaseEntity {
private String email;
}
@Entity
public class Seller extends BaseEntiry {
private String shopName;
}
BaseEntity에 공통적인 정보를 정의하여 자식 엔티티들은 상속을 통해 BaseEntity의 매핑정보를 물려받았다.
부모로부터 물려받은 정보를 재정의 하기 위해서는 아래의 어노테이션을 사용하면 된다.
- @AttributeOverrides
- @AssociationOverrides
복합 키와 식별 관계 매핑
테이블 사이에는 외래 키가 기본 키가 속하는지의 여부에 따라 식별/비식별 관계로 나눈다.
비식별 관계는 외래 키의 nullable에 따라 필수적/선택적 비식별 관계로 구분된다.
비식별관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 것을 추천한다.
비식별 관계 매핑 (복합 키)
@Entity
public class Parent {
@Id
private String id;
@Id
private String id2;
}
복합 키를 기본 키로 엔티티에 매핑하기 위해서 위와 같은 코드를 작성하면 매핑 예외가 발생한다.
JPA에서는 영속성 컨텍스트에서 엔티티를 구분하기 위해 hashCode() , equals() 메서드를 사용한다.
따라서 복합 키를 지원하는 @IdClass, @EmbeddedId를 사용하고 hashCode(), equals()메서드를 오버라이드 해야한다.
@IdClass
@Entity
@IdClass(ParentId.class)
public class Parent {
@Id
@Column(name = "PARENT_ID1")
private String id1;
@Id
@Column(name = "PARENT_ID1")
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() {...}
@Override
public int hashCode() {...}
}
@IdClass를 사용할 때 식별자 클래스는 아래의 조건을 만족해야 한다.
- 식별자 클래스의 속성명 == 엔티티 클래스의 속성명
- Serializable을 구현
- equals, hashCode를 구현
- 기본 생성자 존재
- public 클래스
// 엔티티 등록 전 ParentId 인스턴스를 만들어서 엔티티 클래스의 key로 사용
Parent parent = new Parent();
parent.setId1("id1");
parent.setId2("id2");
parent.setName("name");
em.persist(parent);
// Parent를 복합 키로 조회
ParentId parentId = new ParentId("id1", "id2");
Parent parent = em.find(Parent.class, parentId);
// Child class에서 연관관계를 매핑할 때 @JoinColumns로 복합 키 명시
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID1", referencedCloumnName = "PARENT_ID1"),
@JoinColumn(name = "PARENT_ID2", referencedCloumnName = "PARENT_ID2")
})
private Parent parent;
위와 같이 @IdClass로 구현한 복합키 엔티티를 핸들링할 수 있다.
@EmbeddedId
@EmbeddedId는 @IdClass보다는 좀 더 객체지향적인 방법이다.
@Entity
public class Parent {
@EmbeddedId
priavte 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
...
}
@EmbeddedId는 직접 식별자 클래스에 기본 키를 직접 매핑한다. @EmbeddedId를 적용한 식별자 클래스는 아래의 조건을 만족해야 한다.
- @Embeddable을 붙여준다.
- Serializable을 구현
- equals, hashCode를 구현
- 기본 생성자
- public 클래스
// 엔티티 등록
Parent parent = new Parent();
ParentId parentId = new ParentId("id1", "id2");
parentId.setId(parentId);
parent.setName("name");
em.persist(parent);
// Parent를 복합 키로 조회
ParentId parentId = new ParentId("id1", "id2");
Parent parent = em.find(Parent.class, parentId);
위와 같이 @EmbeddedId로 구현한 복합키 엔티티를 핸들링할 수 있다.
복합 키와 equals(), hashCode()
ParentId id1 = new ParentId("id1", "id2");
ParentId id2 = new ParentId("id1", "id2");
Assertions.assertTrue(id1.equals(id2));
JPA에서 같은 값을 가진 식별자 클래스는 같다고 봐야하기 때문에 equals(...) 메서드는 true를 반환해야한다.
따라서 영속성 컨텍스트에서 엔티티를 정상적으로 관리하기 위해서는 equals(), hashCode() 메서드를 오버라이드해야한다.
@IdClass vs @EmbeddedId
이 두 어노테이션은 각각의 장단점이 존재하며 본인의 취향에 맞는 것을 일관성 있게 사용하면된다.
EmbeddedId는 보다 객체지향적이지만 특정 JPQL을 사용할 때에는 더 길어질 수 있다.
식별 관계 매핑 (복합 키)
@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")
private Parent parent;
@Id @Column(name = "CHILD_ID")
private String childId;
private String name;
...
}
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 = "GRANDCHILD_ID")
private String id;
private String name;
...
}
public class GrandChildId implements Serializable {
private ChildId child;
private String id;
// equals, hashCode
...
}
@EmbeddedId를 사용하는 방법
@Entity
public class Parent {
@Id @Column(name = "PARENT_ID")
private String id;
private String name;
...
}
@Entity
public class Child {
@EmbeddedId
private ChildId id;
@MapsId("parentId")
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
private String name;
...
}
@Embeddable
public class ChildId implements Serializable {
private String parentId;
@Column(name = "CHILD_ID")
private String id;
// equals, hashCode
...
}
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("childId")
@ManyToOne
@JoinColumns({
@JoinColumn(name = "PARENT_ID")
@JoinColumn(name = "CHILD_ID")
})
private Child child;
private String name;
...
}
@Embeddable
public class GrandChildId implements Serializable {
private ChildId child;
@Column(name = "GRANDCHILD_ID")
private String id;
// equals, hashCode
...
}
외래키가 복합키에 포함되는 경우 복합키 클래스와 엔티티 클래스에 두 번 필드를 정의하였다.
그리고 엔티티 클래스에 @MapsId 어노테이션을 설정해서 두 필드를 매핑하는 과정을 거쳤다.
개인적으로는 @IdClass 보다는 @EmbeddedId를 사용하는 편이 코드가 더 쉽게 읽히고 관계를 이해하기 편해서 선호한다.
비식별 관계로 구현 (복합 키 X)
@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;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
private String name;
...
}
@Entity
public class GrandChild {
@Id @GeneratedValue
@Column(name = "GRANDCHILD_ID")
private Long id;
@ManyToOne
@JoinColumn(name = "CHILD_ID")
private Child child;
private String name;
...
}
대리키를 활용하여 비식별 관계를 매핑해보니 복합 키를 사용한 코드보다 훨씬 더 깔끔해졌다.
복합 키가 없기 때문에 복합 키 클래스를 따로 만들지 않아도 된다.
식별, 비식별 관계의 장단점
데이터베이스 설계 관점에서 보면 아래와 같은 이유로 비식별 관계를 더 선호한다.
- 식별 관계는 하위 테이블로 갈 수록 기본 키 컬럼이 점점 늘어난다. (조인 성능 저하)
- 식별 관계는 복합 기본 키를 사용하는 경우가 많아진다.
- 식별 관계는 부모 테이블 기본 키를 자식 테이블 기본 키로 사용하므로 테이블 구조가 유연하지 못하다.
- 비식별 관계는 대리 키를 사용하기 때문에 비즈니스에 의존없는 설계가 가능하다.
객체 관계 관점에서 보더라도 아래와 같은 이유로 비식별 관계를 더 선호한다.
- 식별 관계는 복합 키 클래스가 필요한 경우가 많아져서 더 많은 리소스를 쓰게된다.
- 비식별 관계는 JPA에서 @GeneratedValue를 사용할 수 있으므로 편리하다.
하지만 식별 관계가 가지는 장점도 분명히 존재한다.
- 특정 상황에서 하위 테이블만 조회하더라도 원하는 정보를 검색할 수 있다.
⇒ 전반적으로 JPA에서 추천하는 방법은 비식별 관계, Long 타입 대리 키를 사용하는 방법이다.
조인 테이블
데이터베이스 테이블의 연관관계를 설계하는 방법은 크게 2가지이다.
- 조인 컬럼 사용 (외래 키)
- 조인 테이블 사용 (테이블)
일반적으로 다대다 관계에서 조인테이블을 사용하며그 외의 연관관계는 조인 컬럼을 사용한다.
@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<>();
...
}
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
...
}
엔티티 하나에 여러 테이블 매핑
@SecondaryTable을 사용하면 하나의 엔티티에 여러 테이블을 매핑할 수 있다.
예를 들어 Board Entity - (Board Table + Board_Detail Table)을 매핑해보자.
@Entity
@Table(name = "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;
...
}
@Table을 사용해서 BOARD 테이블과 매핑하고, @SecondaryTable을 사용해서 BOARD_DETAIL 테이블과도 매핑했다.
pkJoinColumns 속성은 매핑할 다른 테이블의 기본 키 컬럼 속성을 명시한다.
그리고 content 필드에 @Column의 table 속성으로 BOARD_DETAIL 테이블과 따로 매핑을 해주었다.
그러나 이와 같은 방법은 항상 두번 조회하기 때문에 최적화가 어렵고, 일대일 매핑에 비해 유연하지도 못하다.
따라서 여러 테이블을 매핑하는 것은 권장하지 않는다.
다음 글: https://gojs.tistory.com/48
JPA 프록시와 페치 전략
특정 엔티티를 조회할 때 연관된 엔티티를 항상 같이 조회하는 것은 아니다.한번에 모든 연관관계를 로딩하게 되면 너무 많은 데이터를 조회하게 되어서 OOM이 발생할 수도 있다.그렇다면 JPA에
gojs.tistory.com
'공부 > JPA' 카테고리의 다른 글
JPA 값 타입 구현하기 (0) | 2024.07.27 |
---|---|
JPA 프록시와 로딩 전략 (0) | 2024.07.27 |
JPA Entity 간 연관관계 매핑 (1) (0) | 2024.07.26 |
JPA Entity 구성하기 (0) | 2024.07.24 |
JPA 영속성 관리 기법 (0) | 2024.07.23 |