7장-고급매핑

자바 ORM 표준 JPA 프로그래밍 7장을 요약한 내용 입니다.

이 장에서 다룰 고급 매핑

  • 상속 관계 매핑: 객체의 상속 관계에 대한 데이터베이스 표현
  • @MappedSupperClass: 등록일, 수정일 같이 여러 공통으로 사용하는 매핑 정보만 상속받고 싶을때
  • 복합 키와 식별 관계 매핑: 데이터베이서의 식별자(id) 값이 하나 이상일때 매핑하는 방법
  • 조인 테이블: 연관관계를 관리하는 연결 테이블(매핑 테이블)을 두는 방법
  • 엔티티 하나에 여러 테이블을 매핑하기

7.1 상속 관계 매핑

관계형 데이터베이스에는 상속이라는 개념이 없다.

대신에 아래 그림과 같은 슈퍼타입-서브타입 관계(super type - sub type Relationship)이라는 모델링 기법이 있다.

https://user-images.githubusercontent.com/19490925/81796207-11f9f800-9548-11ea-95dc-5cb2e90b6921.png

https://user-images.githubusercontent.com/19490925/81796276-2e963000-9548-11ea-837b-4efcd899dbb6.png

슈퍼-서브 타입의 논리 모델을 실제 물리 모델인 테이블로 구현할 때는 3가지 방법을 선택할 수 있다.

  • 각각의 테이블로 변환
  • 하나의 테이블로 변환
  • 서브타입 테이블로 변환

7.1.1 조인 전략

조인 전략은 아래 그림과 같이 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략이다. 타입을 구분하기 위한 컬럼이 필요하다.

https://user-images.githubusercontent.com/19490925/81796722-cbf16400-9548-11ea-9f19-c78163f511b2.png

image

@Entity  
@Inheritance(strategy = InheritanceType.JOINED)  
@DiscriminatorColumn(name = "DTYPE")  
public abstract class Item {

}

@Entity
@DiscriminatorValue("A")
public class Album extends Item {

}

기본 값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶다면 @PrimaryKeyJoinColumn을 사용하자

장점

  • 테이블이 정규화 된다
  • 외래 키 참조 무결성 제약조건을 활용할 수 있다
  • 저장공간을 효율적으로 사용한다

단점

  • 조회할때 성능이 저하될 수 있다
  • 조회 쿼리가 복잡하다
  • 데이터를 등록할때 insert sql이 두 번 실행된다

7.1.2 단일 테이블 전략

아래 그림과 같이 테이블 하나를 사용하는 전략

https://user-images.githubusercontent.com/19490925/81797478-cea08900-9549-11ea-969d-3add24db0464.png

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

장점

  • 조인이 필요 없으므로 일반적으로 조회 성능이 빠르다
  • 조회 쿼리가 단순하다

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 NULL 허용해야 한다.
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다

7.1.3 구현 클래스마다 테이블 전략

각각의 엔티티마다 테이블을 만든다

https://user-images.githubusercontent.com/19490925/81797777-2f2fc600-954a-11ea-8623-dac99ba0eceb.png

@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)

각각의 자식 엔티티마다 테이블을 만들기 때문에 일반적으로 추천되지 않는다

장점

  • 서브 타입을 구분해서 처리할 때 효과적이다
  • NOT NULL 제약조건을 사용할 수 있다

단점

  • 여러 자식테이블과 함께 조회할 때 성능이 느리다(UNION 사용이 불가피)
  • 자식 테이블을 통합해서 쿼리하기 어렵다

7.2 @MappedSuperclass

부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶을때 사용한다

추상클래스와 비슷하다.(엔티티는 실제 테이블과 매핑된다)

https://user-images.githubusercontent.com/19490925/81798376-f04e4000-954a-11ea-9852-b70f5e13ce9e.png

https://user-images.githubusercontent.com/19490925/81798417-fb08d500-954a-11ea-9407-f882e36e2e51.png

@MappedSuperclass
public abstract class BaseEntity{
  @Id 
  private Long id;
  private String name;
}

@Entity
public class Member extends BaseEntity {
	// id, name 상속
}

@Entity
public class Seller extends BaseEntity{
	// id, name 상속
}

부모로 부터 물려 받은 매핑정보를 재정의 하려면 @AttributeOverride를 사용한다.

특징

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용한다.
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 Jpql에서 사용할 수 없다
  • 직접적으로 생성할일이 적기 때문에 추상클래스로 만드는것이 적합하다

⭐ 등록일자, 수정일자, 등록자, 수정자와 같은 여러 엔티티를 효율적으로 관리할 수 있다?

다음과 같은 상황에는 어떨까?

  • 등록일시가 필요한 상황
  • 등록일시, 수정일시가 필요한 상황
  • 등록일시, 등록자가 필요한 상황
  • 등록일시, 등록자, 수정일시가 필요한 상황
  • 등록일시, 등록자, 수정일시, 수정자가 필요한 상황

이렇게 요구사항이 다양할 경우 상속 보다는 합성(composition)을 통한 구현이 더 좋지 않을까?

jpa에서 합성은 Embedded objects로 구현되어 있다.

7.3 복합 키와 식별 관계 매핑

7.3.1 식별 관계 vs 비식별 관계

식별 관계

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

https://user-images.githubusercontent.com/19490925/81799665-a49c9600-954c-11ea-855a-90b5c3739138.png

비식별 관계

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

https://user-images.githubusercontent.com/19490925/81799754-c269fb00-954c-11ea-80f8-c26b04685f35.png

최근 추세는 비식별 관계를 주로 사용하고 필요한 경우 식별 관계를 사용하는 추세다.

7.3.2 복합 키: 비식별 관계 매핑

jpa는 둘 이상의 컬럼으로 구성된 복합 기본키 매핑을 위해서 @IdClass@EmbeddedId를 지원한다

@IdClass

https://user-images.githubusercontent.com/19490925/81800022-255b9200-954d-11ea-8c87-77daf04f1965.png

@Entity
@IdClass(ParentId.class)
public class Parent{
  @Id
  private String id1;

  @Id
  private String id2;
}

public class ParentId implements Serializable {
  private String id1;
  private String id2;
}

식별자 클래스는 다음 조건을 만족해야 한다

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 속성명이 같아야 한다.
  • Serializable인터페이스를 구현해야한다.
  • equals, hashcode를 구현해야한다
@Entity
public class Child {
  @ManyToOne
  @JoinColumns({
    @JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1),
    @JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2)
  })
  private Parent parent;
}

부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블 역시 복합 키로 매핑해 주어야 한다

@EmbeddedId 조금 더 객체지향적인 방법

@Entity 
public class Parent {
  @EmbeddedId
  private ParentId id;

}

@Embeddable
public class ParentId implements Serializable {
  private String id1;
  private String id2;
}

사용하기 위한 조건

  • Embeddable annotation을 붙여주어야 한다.
  • Serializable interface를 구현해야한다
  • equals, hashCode를 구현해야한다. → 오버라이딩 하지 않는 다면 == 비교(동일성 비교)를 하기 때문에 의도한대로 동작하지 않을 수 있다

@IdClass vs @EmbeddedId EmbeddId가 더 객체지향적일 수는 있지만 특정 상황에 jpql이 더 길어질 수 있다.

7.3.2 복합 키: 식별 관계 매핑

https://user-images.githubusercontent.com/19490925/81806246-285b8000-9557-11ea-8241-092aa158a496.png

부모, 자식, 손자까지 계속 기본 키를 전달하는 관계

@Entity
public class Parent {
  @Id
  private String id;

}

@Entity
@IdClass(CHildId.class)
public class Child {
  @Id
  @ManyToOne
  @JoinColumn(name = "PARENT_ID")
  private Parent parent;

  @Id
  private String childId;

}

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 ="PARNET_ID")
    @JoinColumn(name = "CHILD_ID")
  })
  private Long id;

  @ManyToOne
  private Child child;
}

이처럼 식별 관계는 기본 키와 외래 키를 같이 매핑해야 한다. 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne을 같이 사용하면 된다

@EmbeddedId와 식별 관계

@Entity
public class Parent {
  @Id
  private String id;
}

@Entity
public class Child {
  @EmbeddedId
  private ChildId id;

  @MapsId("parentId")
  @ManyToOne
  public Parent parent;

}

@Embeddable
public class ChildId implements Serializable {
  private String parentId; // @MapsId("parentId")로 매핑
  private String id;

}

@Entity
public class GrandChild {
  @EmbeddedId
  private GrandChildId id;
  @MapsId("childId") // GrandChildId.childId
  @JoinColumns({
    @JoinColumn(name = "PARENT_ID")
    @JoinColumn(name = "CHILD_ID")
  })
  private Child child;
}

@Embeddable
public class GrandChildId implements Serializable {
  private ChildId childId; // @MapsId("childId")로 매핑

}

7.3.4 비식별 관계로 구현

https://user-images.githubusercontent.com/19490925/81905955-6e205300-9600-11ea-87dc-948e2c9f21d6.png

@Entity
public class Parent {

  @Id @GeneratedValue
  @Column(name = "PARENT_ID")
  private Long id;

}

@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;

}

7.3.5 일대일 식별 관계

https://user-images.githubusercontent.com/19490925/81906394-2221de00-9601-11ea-8916-487a2ac77db5.png

@Entity
public class Board {

  @Id @GeneratedValue
  private Long id;
  
  @OneToOne(mappedBy = "board")
  private BoardDetail boardDetail;
}

@Entity
public class BoardDetail {
  
  @Id
  private Long id;

  @MapsId // BoardDetail.boardId 매핑
  @OneToOne
  @JoinColumn(name = "BOARD_ID")
  private Board board;

}

7.3.6 식별, 비식별 관계의 장 단점

일반적으로 비식별 관계를 선호한다.

  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다
  • 식별 관계와는 다르게 비즈니스와 전혀 관계 없는 값을 키 값으로 사용한다
  • 테이블 구조가 유연하다
  • 복합 키를 만들기 위해서는 많은 노력이 필요하다

물론 식별 키가 지니는 장점도 있다.

  • 키 인덱스를 활용하기 좋고
  • 특정 상황에서 조인 없이 하위테이블을 조회할 수 있다.

7.4 조인 테이블

데이터베이스에서 테이블간의 연관관계를 설계하는 방법은 크게 2가지다

  • 조인 컬럼 사용(외래 키)
  • 조인 테이블 사용(테이블 사용)

조인 컬럼 사용

https://user-images.githubusercontent.com/19490925/81907175-5053ed80-9602-11ea-92e9-0261bce71536.png

  • LOCKER_ID는 NULL 값이 들어가는 경우가 많은 선택적 비식별 관계이다

조인 테이블 사용

https://user-images.githubusercontent.com/19490925/81907305-82fde600-9602-11ea-8bfd-e1ae9f86f562.png

  • 조인 테이블의 단점은 테이블을 추가해야된다는 것이다.
  • 필요에 따라서 사용하는 것이 좋다

7.4.1 일대일 조인 테이블

https://user-images.githubusercontent.com/19490925/81907475-cb1d0880-9602-11ea-9ad7-8fef929b898f.png


@Entity
public class Parent {
  @Id @GeneratedValue
  @Column(name = "PARENT_ID")
  private Long id;
  
  @OneToOne
  @JoinTable(name = "PARENT_CHILD",
    joinColumns = @JoinColumn(name = "PARENT_ID"),
    inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
  )
  private Child child;

}

@Entity
public class Child {
  @Id @GeneratedValue
  @Column(name = "CHILD_ID")
  private Long id;
 
}

@JoinTable속성

  • name: 매핑할 조인 테이블 이름
  • joinColumns: 현재 엔티티를 참조하는 외래 키
  • inverseJoinColumns: 반대방향 엔티티를 참조하는 외래 키

7.4.2 일대다 조인 테이블

https://user-images.githubusercontent.com/19490925/81907924-77f78580-9603-11ea-9f7d-03e9f7397a23.png


@Entity
public class Parent {
  @Id @GeneratedValue
  @Column(name = "PARENT_ID")
  private Long id;
  
  @OneToMany
  @JoinTable(name = "PARENT_CHILD",
    joinColumns = @JoinColumn(name = "PARENT_ID"),
    inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
  )
  private Child child;
}

@Entity
public class Child {
  @Id @GeneratedValue
  @Column(name = "CHILD_ID")
  private Long id;
 
 
}

7.4.3 다대일 조인 테이블


@Entity
public class Parent {
  @Id @GeneratedValue
  @Column(name = "PARENT_ID")
  private Long id;
  
  @OneToMany(mappedBy = "parent")
  private List<Child> child;

}

@Entity
public class Child {
  @Id @GeneratedValue
  @Column(name = "CHILD_ID")
  private Long id;
 
  @ManyToOne(optional = false)
  @JoinTable(name = "PARENT_CHILD",
    joinColumns = @JoinColumn(name = "CHILD_ID"),
    inverseJoinColumns = @JoinColumn(name = "PARENT_ID")
  )
  private Parent parent;
 
}

7.4.4 다대다 조인 테이블

https://user-images.githubusercontent.com/19490925/81908992-f56fc580-9604-11ea-9de6-0659567b00b3.png


@Entity
public class Parent {
  @Id @GeneratedValue
  @Column(name = "PARENT_ID")
  private Long id;
  
  @ManyToOne
  @JoinTable(name = "PARENT_CHILD",
    joinColumns = @JoinColumn(name = "PARENT_ID"),
    inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
  )
  private List<Child> child;

}

@Entity
public class Child {
  @Id @GeneratedValue
  @Column(name = "CHILD_ID")
  private Long id;
   
}

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

https://user-images.githubusercontent.com/19490925/81909241-44b5f600-9605-11ea-80cd-2eb3d88fe66e.png

@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
  pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board {
  @Id @GeneratedValue
  private Long id;

  @Column(table = "BOARD_DETAIL") // 테이블 지정을 통해 특정 테이블의 컬럼 정보를 매핑
  private String content

}

@SecondaryTable보다는 두 테이블을 각각의 엔티티에 매핑후 일대일 연관관계로 만드는 것을 권장한다