본문 바로가기

JPA

[개념] JPA 프로그래밍 - 07. 고급매핑

상속 관계 매핑

조인 전략

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

  • 주의사항
    • 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없음
      따라서 타입을 구분하는 컬럼을 추가해야 한다. (DTYPE 컬럼)
@Entity
@Inheritance(strategy=IngeritanceType.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;
}
  • @Inheritance(strategy=InheritanceType.JOINED) : 상속매핑은 부모클래스에 @Inheritance를 사용해야 함, 조인전략을 사용하므로 InheritanceType.JOINED를 사용
  • @DiscriminatorColumn(name="DTYPE") : 부모클래스에 자식테이블을 구분할 수 있는 구분 컬럼을 지정, 기본값이 DTYPE이므로 @DiscriminatorColumn으로 줄여 사용해도 됨.
  • @DiscriminatorValue("M") : 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
장점 단점

테이블이 정규화 된다.

조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
외래키 참조 무결성 제약조건을 활용할 수 있다. 조회 쿼리가 복잡하다.
저장공간을 효율적으로 사용한다. 데이터를 등록할 INSERT SQL을 두 번 실행한다.

JPA 표준 명세는 구분컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분컬럼(@DiscriminatorColumn) 없이도 동작

 

단일 테이블 전략

  • 단일 테이블 전략은 이름 그대로 테이블을 하나만 사용하고, 구분컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다.
  • 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠르다.
  • 주의할 점은 자식 엔티티가 매핑한 컬럼을 모두 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;
    private int price;
}
 
@Entity
@DiscriminatorValue("A")
public class Album extends Item {...}
 
@Entity
@DiscriminatorValue("M")
public class Movie extends Item {...}
 
@Entity
@DiscriminatorValue("B")
public class Book extends Item {...}

 

장점 단점
조인이 필요 없으므로 일반적으로 조회 성능이 빠르다. 자식 엔티티가 매핑한 컬럼은 모두 null 허용해야한다.
조회 쿼리가 단순하다. 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. 그러므로 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.

 

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

  • 자식 엔티티마다 테이블을 만들고 자식 테이블 각각에 필요한 컬럼이 모두 있다.
  • 구분 컬럼을 사용하지 않는다.
  • 일반적으로 추천하지 않는 전략...
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
@DiscriminatorColumn(name="DTYPE")
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 {...}
 
@Entity
public class Movie extends Item {...}
 
@Entity
public class Book extends Item {...}

 

장점 단점
서브 타입을 구분해서 처리할 때 효과적이다. 여러 자식 테이블을 함께 조회할 때 성능이 느리다.
not null 제약조건을 사용할 수 있다. 자식 테이블을 통합해서 쿼리하기 어렵다.

@MappedSuperclass

  • 부모클래스는 테이블과 매핑하지 않고 자식클래스에게 매핑 정보만 제공하고 싶을 때 사용
  • @Entity는 실제테이블과 매핑되지만 @MappedSuperclass는 실제테이블과는 매핑되지 않음
@MappedSuperclass
public abstract class BaseEntity {
    @Id @GeneratedValue
    private Long id; 
    private String name;
}
 
@Entity
@AttributeOverride(name="id", column=@Column(name="MEMBER_ID"))  // 부모에게 물려받은 매핑정보를 재정의하는 방법 
public class Member extends BaseEntity {...}
 
@Entity
public class Seller extends BaseEntity {...}

복합키와 식별 관계 매핑

최근 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세

JPA는 식별 관계와 비식별 관계 모두 지원

 

복합키:비식별 관계 매핑

@IdClass

  • 관계형 데이터베이스에 가까운 방법
  • 부모클래스
@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id
    @Column(name="PARENT_ID1")
    private String id1;  // ParentId.id1과 연결
 
    @Id
    @Column(name="PARENT_ID2")
    private String id2;  // ParentId.id2와 연결
 
    private String name;
}
 
//식별자 클래스 
public class ParentId implements Serializable {
    private String id1;  // Parent.id1 매핑
    private String id2;  // Parent.id2 매핑
 
    @Override
    public boolean equals(Object o) {...}
 
    @Override
    public int hashCode() {...}
}

 

  • 식별자 클래스 조건
    • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자 속성명이 같아야 한다.
    • Serializable 인터페이스를 구현해야 한다.
    • equals, hashCode를 구현해야 한다.
    • 기본 생성자가 있어야 한다.
    • 식별자 클래스는 public 이어야 한다.
  • 자식클래스
@Entity
public class Child {
    @Id
    private String id;
 
    @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;
 
    private String name;
}
 
@Embeddable
public class ParentId implements Serializable {
    @Column(name="PARENT_ID1")
    private String id1;
    @Column(name="PARENT_ID2")
    private String id2;
 
    //equals and hashCode 구현
}

 

  • 식별자 클래스 조건
    • @Embeddable 어노테이션을 붙여주어야 한다.
    • Serializable 인터페이스를 구현해야 한다.
    • equals, hashCode를 구현해야한다.
    • 기본 생성자가 있어야 한다.
    • 식별자 클래스는 public이어야 한다.

@IdClass와 @EmbeddedId 는 각각 장단점이 있으므로 본인 취향에 맞는 것을 일관성 있게 사용하면 된다!

 

복합키:비식별 관계 매핑

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

@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="GRANDCHILD_ID")
    private String id;
 
    private String name;
}
 
//손자ID
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")  // ChildId.parentID 매핑
    @ManyToOne
    @JoinColumn(name="PARENT_ID")
    public Parent parent;
 
    private String name;
}
 
//자식 ID
@Embeddable
public class ChildId implements Serializable {
    private String parentId;  // @MapsId("parentId")로 매핑
 
    @Column(name="CHILD_ID")  
    private String id;
 
    //equals, hashCode
}
 
//손자
@Entity
public class GrandChild {
    @EmbeddedId
    private GrandChildId id;
    
    @MapsId("childId")  // GrandChildId.childId 매핑
    @ManyToOne
    @JoinColumns({
          @JoinColumn(name="PARENT_ID"),
          @JoinColumn(name="CHILD_ID")})
    private Child child;
 
    private String name;
}
 
//손자ID
@Embeddable
public class GrandChildId implements Serializable {
    private ChildId childId;  // @MapsId("childId)로 매핑
 
    @Column(name="GRANDCHILD_ID")  
    private String id;
 
    //equals, hashCode
}

 

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

  • 식별 관계보다는 비식별 관계를 선호
    • 식별관계는 부모테이블의 기본키를 자식테이블로 전파하면서 점점 늘어날 수 있다. 결국 조인할 때 SQL이 복잡해지고 기본키 인덱스가 불필요하게 커질 수 있다.
    • 식별관계는 2개이상의 컬럼을 합하여 복합키를 만들어야 하는 경우가 많다. (복잡)
    • 식별관계는 비식별관계보다 테이블 구조가 유연하지 못하다.
  • 될 수 있으면 비식별 관계를 사용하고 기본키는 Long 타입의 대리키(@GenerateValue)를 사용하자!

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

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

 

 

출처도서 : 자바 ORM 표준 JPA 프로그래밍 - 김영한 지음