본문 바로가기
Spring JPA

고급 매핑 - 10

by 홍굴이 2021. 7. 12.

상속 관계 매핑 - 조인 전략

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

※ 객체는 타입으로 구분할 수 있지만 테이블은 타입의 개념이 없으므로 타입을 구분하는 컬럼을 추가해야 한다.

 

조인 전략 매핑

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

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String price;
    
}
@Entity
@DiscriminatorValue("album")
public class Album extends Item{

    private String artist;

}
@Entity
@DiscriminatorValue("movie")
public class Movie extends Item {

    private String actor;
    private String director;

}
@Entity
@DiscriminatorValue("book")
@PrimaryKeyJoinColumn(name = "book_id")	// id 재정의
public class Book extends Item {

    private String author;
    private String isbn;

}
  • @Inheritance(strategy = InheritanceType.JOINED) : 부모 클래스에 상속 매핑 정의 -> 조인 전략 사용
  • @DiscriminatorColumn(name = "TYPE") : 부모 클래스에 구분 컬럼을 지정 -> 자식 테이블 구분 가능
  • @DiscriminatorValue("...") : 엔티티를 저장할 때 구분 컬럼에 입력할 값
  • @PrimaryKeyJoinColumn(name = "..._id") : 자식 테이블의 기본 키 컬럼명 변경

장점

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

단점

  • 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있다.
  • 조회 쿼리가 복잡하다.
  • 데이터를 등록한 insert SQL을 두번 실행한다.

상속 관계 매핑 - 단일 테이블 전략

테이블을 하나만 사용 하여 구분 컬럼(type)으로 어떤 자식 데이터가 저장되었는지 구분한다.

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

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String price;
    
}
@Entity
@DiscriminatorValue("album")
public class Album extends Item{

    private String artist;

}
@Entity
@DiscriminatorValue("movie")
public class Movie extends Item {

    private String actor;
    private String director;

}
@Entity
@DiscriminatorValue("book")
public class Book extends Item {

    private String author;
    private String isbn;

}

장점

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

단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다.
  • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있다. -> 상황에 따라서는 조회 성능이 오히려 느려질 수 있다.

특징

  • @DiscriminatorColumn 필수 사용 -> 구분 컬럼을 꼭 사용해야 한다. 
  • @DiscriminatorColumn을 지정하지 않으면 기본으로 엔티티 이름을 사용한다.

상속 관계 매핑 - 구현 클래스마다 테이블 전략

자식 엔티티마다 테이블을 만든다.

일반적으로 추천하지 않는 전략이다.

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

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String price;
    
}
@Entity
public class Album extends Item{

    private String artist;

}
@Entity
public class Movie extends Item {

    private String actor;
    private String director;

}
@Entity
public class Book extends Item {

    private String author;
    private String isbn;

}

장점

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

단점

  • 여러 자식 테이블을 함께 조회할 때 성능이 느리다.(SQL에 UNION을 사용해야 한다.)
  • 자식 테이블을 통합해서 쿼리하기 어렵다.

특징

  • 구분 컬럼을 사용하지 않는다.

@MappedSuperclass

부모 클래스가 테이블과 매핑하지 않고 상속 받는 자식 클래스에게 매핑 정보만 제공

즉, 단순히 매핑 정보를 상속할 목적으로만 사용한다.

@MappedSuperclass
public abstract class BaseEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;

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

    private String email;

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

    private String shopName;

}

특징

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용
  • @MappedSuperClass로 지정한 클래스는 엔티티가 아니므로 entityManger.find() 혹은 JPQL에서 사용할 수 없다.
  • 직접 생성해서 사용할 일이 없으므로 추상 클래스로 만드는 것을 권장
  • 부모로부터 물려받은 매핑 정보를 재정의하려면 @AttributeOverrides나 @AttributeOverride를 사용한다.
  • 부모로부터 물려받은 연관관계를 재정의하려면 @AssociationOverrides나 @AssociationOverride를 사용한다.
  • 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통적으로 사용하는 속성을 효과적으로 관리

재정의 예시

@Entity
@AttributeOverride(name = "id", column = @Column(name = "user_id"))
public class User extends BaseEntity {
}

등록일자, 수정일자 사용 예시

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    protected LocalDateTime createAt = null;

    @LastModifiedDate
    @Column(name = "last_modified_at", nullable = false)
    protected LocalDateTime lastModifiedAt = null;

}
  • @EntityListeners(AuditingEntityListener.class) : Spring Data JPA에서 시간에 대해서 자동으로 값을 넣어주는 기능
  • @CreatedDate : Entity가 생성되어 저장될 때 시간이 자동 저장
  • @LastModifiedDate : 조회한 Entity의 값을 변경할 때 시간이 자동 저장

 

복합키 : 비식별 관계 매핑

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

@IdClass or @EmbeddedId 두 가지 방식을 지원

 

@IdClass

@Entity
@IdClass(ParentId.class)
public class Parent {

    @Id
    @Column(name = "parent_mom_id")
    private String momId;	// ParentId.momId 와 연결

    @Id
    @Column(name = "parent_dad_id")
    private String dadId;	// ParentId.dadId 와 연결

    private String momName;
    private String dadName;
    
}
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
public class ParentId implements Serializable {

    private String momId;	//Parent.momId 매핑
    private String dadId;	//Parent.dadId 매핑

}
@Entity
public class Child {

    @Id
    private String childId;

    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "parent_mom_id", referencedColumnName = "parent_mom_id"),
            @JoinColumn(name = "parent_dad_id", referencedColumnName = "parent_dad_id")
    })
    private Parent parent;
}

조건

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다.
  • Serializable 인터페이스를 구현해야 한다.
  • equals, hashCode를 구현해야 한다.
  • 기본 생성자가 있어야 한다.
  • 식별자 클래스는 public이어야 한다.

@EmbeddedId

@Entity
public class Parent {

    @EmbeddedId
    private ParentId parentId;

    private String momName;
    private String dadName;
    
}
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Embeddable
public class ParentId implements Serializable {

    @Column(name = "parent_mom_id")
    private String momId;
    @Column(name = "parent_dad_id")
    private String dadId;

}

@EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑한다.

조건

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

 

복합 키와 equals(), hashCode()

영속성 컨텍스트는 엔티티의 식별자를 키로 사용해서 엔티티를 관리한다.

그리고 식별자를 비교할 때 equlas()와 hashCode()를 사용한다.

따라서 식별자 객체의 동등성이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는 데 심각한 문제가 발생한다.

 

더보기

참고
복합 키에는 @GeneratedValue를 사용할 수 없다.


복합 키 : 식별 관계 매핑

@IdClass와 식별 관계

@Entity
public class Parent {

    @Id
    @Column(name = "parent_id")
    private String parendId;

    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;
}
@NoArgsConstructor
@AllArgsConstructor
@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
    @Column(name = "grand_child_id")
    private String grandChildId;

    private String name;
}
public class GrandChildId implements Serializable {

    private ChildId child;        //GrandChild.child 매핑
    private String grandChildId;    //GrandChild.id 매핑
}

식별 관계는 위의 코드와 같이 기본 키와 외래 키를 같이 매핑해야 한다.

@Id와 연관관계 매핑인 @ManyToOne을 같이 사용하면 된다.

 

@EmbeddedId와 식별 관계

@Entity
public class Parent {

    @Id
    @Column(name = "parent_id")
    private String parendId;

    private String name;

}
@Entity
public class Child {

    @MapsId("parentId") //ChildId.parentId 매핑
    @ManyToOne
    @JoinColumn(name = "parent_id")
    public Parent parent;

    @EmbeddedId
    private ChildId childId;
}
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Embeddable
public class ChildId implements Serializable {

    private String parentId;    //@MapsId("parentId") 매핑

    @Column(name = "child_id")
    private String childId;

}
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {

    @MapsId("childId")  //GrandChild.childId 매핑
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "parent_id"),
            @JoinColumn(name = "child_id")
    })
    private Child child;

    @EmbeddedId
    private GrandChildId grandChildId;

    private String name;
}
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Embeddable
public class GrandChildId implements Serializable {

    private ChildId child;        //@MapsId("childId") 매핑

    @Column(name = "grand_child_id")
    private String grandChildId;

}

@Id 대신에 @MapsId를 사용하면서 외래 키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 의미이다.

@MapsId의 속성 값은 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드를 지정하면 된다.

 

일대일 식별 관계

일대일 식별 관계는 자식 테이블의 기본 키 값으로 부모 테이블의 기본 키 값만 사용한다.

@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@Embeddable
public class GrandChildId implements Serializable {

    private ChildId child;        //@MapsId("childId") 매핑

    @Column(name = "grand_child_id")
    private String grandChildId;

}
@Entity
public class BoardDetail {

    @Id
    private Long boardId;

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

    private String content;
}

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

데이터베이스 관점 -> 비식별 관계 선호

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 점점 늘어난다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다.
  • 식별 관계는 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다.
  • 비식별 관계는 기본 키로 비즈니스와 전혀 관계없는 대리 키를 주로 사용한다.
  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용하므로 테이블 구조가 유연하지 못하다.

객체 관계 매핑 관점 -> 비식별 관계 선호

  • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 키를 사용한다.
    즉, JPA에서 복합 키는 별도의 복합 키 클래스를 만들어서 사용해야 한다.
  • 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 @JPA는 @GenerateValue처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.

식별 관계가 가지는 장점

  • 기본 키 인덱스를 활용하기 좋다.
  • 상위 테이블의 기본 키 컬럼을 자식, 손자 테이블들이 가지고 있으므로 특정한 상황에 조인 없이 하위 테이블만으로 검색을 완료할 수 있다.

일대일 조인 테이블

 

 

@Entity
public class Parent {

    @Id
    @GeneratedValue
    @Column(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;

}
@Entity
public class Child {

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

    private String name;

    //양방향이 아닐 경우 생략
    @OneToOne(mappedBy = "child")
    private Parent parent;
}

 

@JoinTable 속성

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

일대다 조인 테이블

일대다 관계를 만들려면 조인 테이블의 컬럼 중 다(N)와 관련된 컬럼에 유니크 제약조건을 걸어야 한다.

@Entity
public class Parent {

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

    private String name;

    @OneToMany
    @JoinTable(name = "parent_child",
            joinColumns = @JoinColumn(name = "parent_id"),
            inverseJoinColumns = @JoinColumn(name = "child_id"))
    private List<Child> child = new ArrayList<Child>();

}
@Entity
public class Child {

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

    private String name;

}

다대일 조인 테이블

@Entity
public class Parent {

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

    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> child = new ArrayList<Child>();

}
@Entity
public class Child {

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

    private String name;

    @ManyToOne(optional = false)
    @JoinTable(name = "parent_child",
            joinColumns = @JoinColumn(name = "child_id"),
            inverseJoinColumns = @JoinColumn(name = "parent_id"))
    private Parent parent;
}

다대다 조인 테이블

 

@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<Child>();

}
@Entity
public class Child {

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

    private String name;
    
}
더보기

참고

조인 테이블에 컬럼을 추가하면 @JoinTable 전략을 사용할 수 없다.
대신에 새로운 엔티티를 만들어서 조인 테이블과 매핑해야 한다.


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

@Entity
@Table(name = "tbl_board")
@SecondaryTable(name = "tbl_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;
}
  • @SecondaryTable.name : 매핑할 다른 테이블의 이름, 예제에서는 테이블명을 tbl_board_detail로 지정했다.
  • @SecondaryTable.pkJoinColumns : 매핑할 다른 테이블의 기본 키 컬럼 속성, 예제에서는 기본 키 컬럼명을 board_detail_id로 지정했다.

'Spring JPA' 카테고리의 다른 글

값 타입 - 12  (0) 2021.07.16
프록시와 연관관계 관리 - 11  (0) 2021.07.13
다양한 연관관계 매핑 - 9  (0) 2021.07.09
연관관계 매핑 기초 - 8  (0) 2021.07.06
엔티티 매핑 - 7  (0) 2021.07.03

댓글