[JPA] Chapter 7. 고급 매핑

joyful·2021년 7월 31일
0

JPA

목록 보기
11/18

들어가기 앞서

이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.


7.1 상속 관계 매핑

객체의 상속 관계를 데이터베이스에 어떻게 매핑할 것인가

  • 관계형 데이터베이스에는 상속이라는 개념이 존재하지 않음
  • 슈퍼타입 서브타입 관계 모델링 ≒ 상속

📊 슈퍼타입 서브타입 논리 모델

📊 객체 상속 모델


7.1.1 조인 전략(Joined Strategy)

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

  • 조회할 때 조인을 자주 사용
  • 테이블은 '타입'의 개념 x → 타입 구분 컬럼 추가 필요

💻 조인 전략 매핑

/**
 * 상품
 */
@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;    //배우
}
  • @Inheritance
    • 상속 매핑 시 부모 클래스에 사용
    • strategy = InheritanceType.JOINED → 매핑 전략으로 조인 전략 사용
  • @DiscriminatorColumn
    • 부모 클래스에 구분 컬럼 지정 → 저장된 자식 테이블 구분
    • 기본값 : DTYPE
  • @DiscriminatorValue : 엔티티 저장 시 구분 컬럼에 입력할 값 지정
    ex) @DiscriminatorValue("M") → 영화 엔티티 저장 시 구분 컬럼인 DTYPE에 값 M 저장

✅ ID 재정의

  • 기본값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용
  • @PrimaryKeyJoinColumn → 자식 테이블의 기본 키 컬럼명 변경
@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID")  //ID 재정의
public class Book extends Item {

    private String author; //작가
    private String isbn;   //ISBN
    ...
}

✅ 장점

  • 테이블이 정규화 됨
  • 외래 키 참조 무결성 제약조건 활용 가능
  • 저장공간을 효율적으로 사용

✅ 단점

  • 조회 시 조인 사용 횟수 多 → 성능 저하
  • 복잡한 조회 쿼리
  • INSERT SQL 두 번 실행

✅ 특징

  • JPA 표준 명세 → 구분 컬럼 사용
  • 하이버네이트를 포함한 몇몇 구현체 → 구분 컬럼 없이도 동작

✅ 관련 어노테이션

  • @PrimaryKeyJoinColumn
  • @DiscriminatorColumn
  • @DiscriminatorValue

7.1.2 단일 테이블 전략(Single-Table Strategy)

테이블을 하나만 사용하는 전략

  • 구분 컬럼(DTYPE)으로 저장된 자식 데이터 구분
  • 조회 시 조인 사용 x → 가장 빠름

  • 자식 엔티티 매핑 컬럼 → null 허용 필수
    ex) Book 엔티티 저장 시
    • ITEM 테이블의 AUTHOR, ISBN 컬럼만 사용
    • ARTIST, DIRECTOR, ACTOR 컬럼 사용 x → 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 { ... }
  • 테이블 하나에 모든 것 통합 → 구분 컬럼 사용 필수

✅ 장점

  • 조인 필요 x → 조회 성능 빠름
  • 조회 쿼리 단순

✅ 단점

  • 자식 엔티티가 매핑한 컬럼은 모두 null 허용 필수
  • 단일 테이블에 모든 것 저장하므로 테이블이 커질 수 있음
    → 조회 성능이 오히려 느려질 수 있음

✅ 특징

  • 구분 컬럼 사용 필수 → @DiscriminatorColumn 설정 필수
  • @DiscriminatorValue 지정 x → 엔티티 이름 기본 사용
    ex) Movie, Album, Book

7.1.3 구현 클래스마다 테이블 전략(Table-per-Concrete-Class Strategy)

자식 엔티티(서브타입)마다 테이블을 생성하는 전략

  • 자식 테이블에 필요한 컬럼 모두 존재

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

/**
 * 상품
 */
@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
@DiscriminatorValue("A")
public class Album extends Item { ... }

/**
 * 영화
 */
@Entity
@DiscriminatorValue("M")
public class Movie extends Item { ... }

/**
 * 책
 */
@Entity
@DiscriminatorValue("B")
public class Book extends Item { ... }
  • 자식 엔티티마다 테이블 생성
  • 추천하지 않음 → 조인이나 단일 테이블 전략 고려

✅ 장점

  • 서브 타입을 구분해서 처리할 때 효과적
  • not null 제약조건 사용 가능

✅ 단점

  • SQL에 UNION 사용 필요
    → 여러 자식 테이블을 함께 조회할 때 성능이 느림
  • 자식 테이블을 통합해서 쿼리하기 어려움

✅ 특징

  • 구분 컬럼을 사용하지 않음


7.2 @MappedSuperclass

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

  • 추상 클래스와 비슷하지만 실제 테이블과 매핑되지 않음
  • 매핑 정보를 상속할 목적으로만 사용

📊 테이블

📊 객체

💻 @MappedSuperclass 매핑

@MappedSuperclass
public abstract class BaseEntity {

    @Id @GeneratedValue
    private Long id;
    private String name;
    ...
}

@Entity
public class Member extends BaseEntity {

    //ID 상속
    //NAME 상속
    private String email;
    ...
}

@Entity
public class Seller extends BaseEntity {

    //ID 상속
    //NAME 상속
    private String shopName;
    ...
}
  • BaseEntity
    • 객체들이 주로 사용하는 공통 매핑 정보 정의
    • @MappedSuperclass 사용
      - 테이블과 매핑할 필요 x
      - 자식 엔티티에게 공통으로 사용되는 매핑 정보 제공


✅ 재정의

애노테이션설명
@AttributeOverrides,
@AttributeOverried
부모로부터 물려받은 매핑 정보 재정의
@AssociationOverrides,
@AssociationOverride
연관관계 재정의

💻 예시 1 - @AttributeOverride

@Entity
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID"))
public class Member extends BaseEntity { ... }

💻 예시 2 - @AttributeOverrides

@Entity
@AttributeOverrides({
    @AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID")),
    @AttributeOverride(name = "name", column = @Column(name = "MEMBER_NAME"))
})
public class Member extends BaseEntity { ... }

✅ 특징

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용함
  • @MappedSuperclass로 지정한 클래스는 엔티티가 아님
    em.find()JPQL에서 사용 불가능
  • 이 클래스를 직접 생성해서 사용하는 일은 거의 없음
    → 추상 클래스로 만들어 사용(권장)

💡 참고

@MappedSuperclass를 사용하면 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있다.

💡 참고

엔티티(@Entity)는 엔티티(@Entity)이거나 @MappedSuperclass로 지정한 클래스만 상속받을 수 있다.



7.3 복합 키와 식별 관계 매핑

7.3.1 식별 관계 vs 비식별 관계

  • 외래 키가 기본 키에 포함되는지 여부에 따라 데이터베이스 테이블 사이의 관계가 구분 된다.

✅ 식별 관계(Identifying Relationship)

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

✅ 비식별 관계(Non-Identifying Relationship)

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

  • 외래 키 NULL 허용 여부에 따라 구분 됨

    구분NULL 허용 여부연관관계 맺기
    필수적 비식별 관계
    (Mandatory)
    불허필수
    선택적 비식별 관계
    (Optional)
    허용선택

💡 참고

  • 데이터베이스 테이블 설계 시 식별 관계와 비식별 관계 중 하나를 선택해야 함
  • 최근에는 비식별 관계를 주로 사용하고, 꼭 필요한 곳에만 식별 관계를 사용하는 추세임
  • JPA는 식별 관계와 비식별 관계 모두 지원함

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

@IdClass

  • 관계형 데이터베이스에 가까운 방법

📊 복합 키 테이블

  • 비식별 관계
  • PARENT → 복합 키 사용
    ∴ 복합 키를 매핑하기 위한 식별자 클래스 별도 생성 필요

💻 부모 클래스

@Entity
@IdClass(ParentId.class)  //ParentId 클래스를 식별자 클래스로 지정
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 매핑
    
    public ParentId() { }
    
    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }
    
    @Override
    public boolean equals(Object o) {...}
    
    @Override
    public int hashCode() {...}
}
  • @IdClass 사용 시 식별자 클래스가 만족해야 하는 조건들
    • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 함
      ex) Parent.id1ParentId.id1, Parent.id2ParentId.id2
    • Serializable 인터페이스를 구현해야 함
    • equals, hashCode 구현해야 함
      → 식별자를 구분하기 위해 둘을 사용하여 동등성 비교를 함
    • 기본 생성자가 존재해야 함
    • 식별자 클래스는 public이어야 함

💻 엔티티 저장

Parent parent = new Parent();
parent.setId1("myId1");  //식별자
parent.setId2("myId2");  //식별자
parent.setName("parentName");
em.persist(parent);
  • em.persist()를 호출하면, 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2 값을 사용하여 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용함

💻 복합 키로 조회

ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);
  • 식별자 클래스인 ParentId를 사용하여 엔티티 조회

💻 자식 클래스

@Entity
public class Child {

    @Id
    private String id;
    
    @ManyToOne
    @JoinColumns ({  //외래 키 여러개 매핑
        @JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1"),
        // ↑ @JoinColumn(name = "PARENT_ID1")과 같음
        @JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2")
    })  //@JoinColumn으로 각각의 외래 키 매핑
    private Parent parent;
}
  • @JoinColumnname 속성과 referencedColumnName 속성 값이 같을 경우, referencedColumnName 생략 가능

@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 구현
    ...
}
  • @EmbeddedId 사용 시 식별자 클래스가 만족해야 하는 조건들
    • @Enbeddable 어노테이션을 붙여주어야 함
    • Serializable 인터페이스를 구현해야 함
    • equals, hashCode를 구현해야 함
    • 기본 생성자가 존재해야 함
    • 식별자 클래스는 public이어야 함

💻 엔티티 저장

Parent parent = new Parent();

//식별자 클래스 `ParentId`를 직접 생성하여 사용
ParentId parentId = new ParentId("myId1", "myId2");
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);

💻 조회

//식별자 클래스 parentId를 직접 사용하여 조회
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parentId);

✅ 복합 키와 equals(), hashCode()

  • 복합 키는 equals()hashCode()를 필수로 구현해야 함
    • 영속성 컨텍스트는 엔티티의 식별자를 키로 사용하여 엔티티를 관리 함
    • 식별자를 비교할 때 equals()hashCode()를 사용
    • 식별자 객체의 동등성(equals 비교)이 지켜지지 않으면 영속성 컨텍스트가 엔티티를 관리하는 데 심각한 문제가 발생하게 됨
      • 예상과 다른 엔티티가 조회됨
      • 엔티티를 찾을 수 없음
  • 식별자 클래스는 보통 equals()hashCode()를 구현할 때 모든 필드를 사용함

@IdClass vs @EmbeddedId

@EmbeddedId@IdClass와 비교해서 더 객체지향적이고 중복도 없으나 특정 상황에 따라 JPQL이 조금 더 길어질 수 있다. 따라서, 취향에 맞는 것을 골라 일관성 있게 사용하도록 한다.

em.createQuery("select p.id.id1, p.id.id2 from Parent p");  //@EmbeddedId
em.createQuery("select p.id1, p.id2 from Parent p");  //@IdClass

💡 참고

복합 키와 이를 구성하는 여러 칼럼 중 어느것에도 @GenerateValue를 사용할 수 없다.


7.3.3. 복합 키: 식별 관계 매핑

📊 식별관계 구현

  • 부모에서 손자까지 기본 키를 전달하는 식별 관계

@IdClass와 식별 관계

💻 @IdClass로 식별 관계 매핑

/*
** 부모
*/
@Entity
public class Parent {

    @Id @Column(name = "PARENT_ID")
    privte 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;  //Child.parent 매핑
    private String childId; //Child.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 childId;  //GrandChild.child 매핑
    private String id; //GrandChild.id 매핑
    
    //equals, hashCode
    ...
}
  • 식별 관계는 기본 키와 외래 키를 같이 매핑해야 함
    @Id@ManyToOne을 사용하여 식별자 및 연관관계 매핑

@EmbeddedId와 식별 관계

💻 @EmbeddedId로 식별 관계 매핑

/*
** 부모
*/
@Entity
public class Parent {

    @Id @Column(name = "PARENT_ID")
    privte 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 GrandChild 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
    ...
}
  • 식별 관계로 사용할 연관관계의 속성에 @MapsId 사용
  • @MapsId
    • 외래 키와 매핑한 연관관계를 기본 키에도 매핑
    • 속성 값으로 @EmbeddedId를 사용한 식별자 클래스의 기본 키 필드 지정

7.3.4 비식별 관계로 구현

📊 식별 관계 테이블을 비식별 관계로 변경


💻 비식별 관계 매핑

/*
** 부모
*/
@Entity
public class Parent {

    @Id @GeneratedValue
    @Column(name = "PARENT_ID")
    privte Long id;
    private String name;
    ...
}

/*
** 자식
*/
@Entity
public class Child {
    
    @Id @GeneratedValue
    @Column(name = "CHILD_ID")
    private Long id;
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    ...
}

/*
** 손자
*/
@Entity
public class GrandChild {

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

    @ManyToOne
    @JoinColumn(name = "CHILD_ID")
    private Child child;
    ...
}
  • 복합 키 존재 x → 복합 키 클래스 생성 필요 x

7.3.5 일대일 식별 관계

  • 자식 테이블의 기본 키 값으로 부모 테이블기본 키 값만 사용
    → 부모 테이블의 기본 키가 복합 키인 경우를 제외하고 자식 테이블의 기본 키는 복합 키로 구성할 필요 x

💻 일대일 식별 관계 매핑

/*
** 부모
*/
@Entity
public class Board {

    @Id @GeneratedValue
    @Column(name = "BOARD_ID")
    privte Long id;
    
    private String title;
    
    @OneToOne(mappedBy = "board")
    private BoardDetail boardDetail;
    ...
}

/*
** 자식
*/
@Entity
public class BoardDetail {
    
    @Id
    private Long boardId;
    
    @MapsId  //BoardDetail.boardId 매핑
    @OneToOne
    @JoinColumn(name = "Board_ID")
    public Board board;
    
    private String content;
    ...
}

💻 일대일 식별 관계 저장

public void save() {
    Board board = new Board();
    board.setTitle("제목");
    em.persist(board);
    
    BoardDetail boardDetail = new BoardDetail();
    boardDetail.setContent("내용");
    boardDetail.setBoard(board);
    em.persist(boardDetail);
}

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

✅ 식별 관계의 단점

  • 데이터베이스 설계 관점

    • 부모 테이블의 기본 키를 자식 테이블로 전파하므로 자식 테이블의 기본 키 컬럼이 증가하게 됨
      → 조인 시 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있음
    • 2개 이상의 컬럼을 합하여 복합 기본 키를 생성해야 하는 경우가 많음
    • 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많음
      • 비즈니스 요구사항은 변할 가능성이 높음
      • 자연 키 컬럼이 자식, 손자까지 전파될 경우 변경하기 어려움
    • 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용
      → 테이블 구조가 유연하지 못함
  • 객체 관계 매핑 관점

    • 일대일 관계를 제외한 나머지 관계는 복합 기본 키를 사용
      → JPA에서는 별도의 복합 키 클래스를 만들어 사용해야 함
      ∴ 컬럼이 하나인 기본 키 매핑보다 번거로움
    • 비식별 관계의 기본 키는 주로 대리 키를 사용
      → JPA에서 편리한 대리 키 생성 방법 제공 ex) @GeneratedValue

위와 같은 이유들로 식별 관계보다는 비식별 관계를 선호함


✅ 식별 관계의 장점

  • 기본 키 인덱스를 활용하기 좋음
    ex) CHILD 테이블의 기본 키 인덱스를 PARENT_ID + CHILD_ID로 구성
    • 부모 아이디가 A인 모든 자식 조회
    • 부모 아이디가 A고 자식 아이디가 B인 자식 조회
  • 특정 상황에 조인 없이 하위 테이블만으로 검색 완료

✅ 정리

  • 되도록 비식별 관계를 사용하고 기본 키Long 타입의 대리 키를 사용하도록 한다.

    • 대리 키는 비즈니스와 관련이 없으므로, 비즈니스가 변경되어도 유연한 대처가 가능함
    • Long 타입은 데이터의 저장 가능 범위가 매우 크므로 데이터 저장시 문제 발생의 소지가 줄어듦
  • 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하도록 한다.

    • 선택적 비식별 관계 : NULL 허용 → 외부 조인
    • 필수적 비식별 관계 : NOT NULL → 내부 조인


7.4 조인 테이블

✅ 조인 컬럼(외래 키) 사용

  • 선택적 비식별 관계
    • 회원이 사물함을 사용하기 전까지는 둘 사이에 관계가 존재하지 않음
      MEMBER 테이블의 LOCKER_ID 외래 키에 null을 입력해두어야 함
    • 회원과 사물함 조인 시 외부 조인(OUTER JOIN)을 사용해야 함
  • 단점
    • 내부 조인 사용 시 사물함과 관계 없는 회원은 조회되지 않음
    • 관계 형성시 외래 키 값 대부분이 null로 저장되는 경우 존재

✅ 조인 테이블 사용

  • 조인 테이블(MEMBER_LOCKER) 추가하여 두 테이블의 외래 키로 연관관계 관리
    MEMBERLOCKER에 외래 키 컬럼 존재 x
  • 회원과 사물함의 데이터를 등록해놓고, 회원이 사물함 선택 시 MEMBER_LOCKER 테이블에 값만 추가하면 됨
  • 단점
    • 테이블을 하나 추가해야 함 → 관리해야 하는 테이블 증가
    • 회원과 사물함 두 테이블을 조인하려면 조인 테이블까지 추가로 조인해야 함

✅ 참고

  • 기본은 조인 컬럼을 사용하고, 필요하다고 판단될 경우 조인 테이블 사용하도록 한다.

  • 객체와 테이블 매핑시 사용하는 애노테이션은 다음과 같다.

    • 조인 컬럼 : @JoinColumn
    • 조인 테이블 : @JoinTable
  • 조인 테이블은 주로 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 사용하며, 그 외 일대일, 일대다, 다대일 관계에서도 사용한다.


7.4.1 일대일 조인 테이블

  • 일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼 각각에 유니크 제약조건을 걸어야 함
  • PARENT_ID는 기본 키이므로 유니크 제약조건이 걸려 있음

💻 일대일 조인 매핑

/*
** 부모
*/
@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;
}

7.4.2 일대다 조인 테이블

  • 조인 테이블의 컬럼 중 다(N)와 관련 CHILD_ID에 유니크 제약조건 형성
  • CHILD_ID는 기본키이므로 유니크 제약조건이 걸려있는 상태

💻 일대다 단방향 조인 테이블 매핑

/*
** 부모
*/
@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;
    ...
}

7.4.3 다대일 조인 테이블

💻 다대일 양방향 조인 테이블 매핑

/*
** 부모
*/
@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;
    ...
}

7.4.4 다대다 조인 테이블

  • 조인 테이블의 두 컬럼을 합하여 하나의 복합 유니크 제약 조건 형성
  • PARENT_ID, CHILD_ID가 복합 기본 키로 유니크 제약 조건이 이미 걸려 있는 상태

💻 다대다 조인 테이블 매핑

/*
** 부모
*/
@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 전략을 사용할 수 없으므로, 새로운 엔티티를 만들어 조인 테이블과 매핑해야 함



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

  • 자주 사용하지는 않음

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

@Entity
@Table(name = "BOARD")  //BOARD 테이블과 매핑
@SecondaryTable(  // BOARD_DETAIL 테이블 추가 매핑
    name = "BOARD_DETAIL",
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID")
)
public class Board {

    @Id @GeneratedValue
    @Column(name = "BOARD_ID")
    private Long id;
    
    private String title;
    
    //BOARD_DETAIL 테이블의 컬럼과 매핑
    //테이블 미지정 시 기본 테이블인 BOARD에 매핑
    @Column(table = "BOARD_DETAIL")
    private String content;
    ...
}
  • @SecondaryTable : 테이블 한 개 추가 매핑

    속성설명
    name매핑할 다른 테이블의 이름
    pkJoinColumns매핑할 다른 테이블의 기본 키 컬럼 속성
  • @SecondaryTables : 테이블 여러개 추가 매핑

💡 참고

  • 엔티티 하나에 여러 테이블을 매핑하는 방법은 항상 두 테이블을 조회하므로 최적화하기 어려움
  • 일대일 매핑은 원하는 부분만 조회 가능하며, 필요시 함께 조회하면 됨

@SecondaryTable을 이용한 매핑보다 일대일 매핑 권장

profile
기쁘게 코딩하고 싶은 백엔드 개발자

0개의 댓글