본문 바로가기

SpringFramework/JPA

JPA - 연관관계 고급매핑

상속 관계 매핑

조인전략

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

    - 객체는 타입으로 구분이 가능하지만, 테이블은 타입의 개념이 없기때문에 구분하는 컬럼을 추가해야한다.

@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 상속 매핑
@DiscriminatorColumn(name = "DTYPE") // 구분 컬럼을 지정
public abstract class Item {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    private int price;
}

@Entity
@DiscriminatorValue("Album") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Album extends Item {
    private String artist;
}

@Entity
@DiscriminatorValue("Movie") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Movie extends Item {
    private String director;

    private String actor;
}

@Entity
@DiscriminatorValue("Book")
@PrimaryKeyJoinColumn(name = "BOOK_ID") // ITEM_ID 기본키 컬럼명을 BOOK_ID로 변경
public class Book extends Item {
    private String author;
    
    private String isbn;
}

 

@Inheritance(strategy = InheritanceType.JOINED)

- 장점

    - 테이블이 정규화

    - 외래키 참조 무결정 제약조건을 활용할 수 있다

    - 저장공간을 효율적으로 사용한다

- 단점

    - 조회할 때 조인이 많이 사용되므로 성능저하가 초래될 수 있음

    - 조회 쿼리가 복잡함

    - 데이터를 등록할 INSERT의 SQL이 두번실행

 

단일 테이블 전략

: 테이블을 하나만 사용하는 전략. 조인을 사용하지 않기 때문에 빠르다.

    - 테이블 하나에 모든 것을 통합하기때문에 구분 컬럼(@DiscriminatorValue)를 필수로 사용해야한다.

    (지정되지 않으면 기본 Class명을 이름으로 사용한다)

    - 자식 Entity가 매핑한 컬럼은 모두 null이 허용되어야 한다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 상속 매핑
@DiscriminatorColumn(name = "DTYPE") // 구분 컬럼을 지정
public abstract class Item {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
    
    private int price;
}

@Entity
@DiscriminatorValue("Album") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Album extends Item {
    private String artist;
}

@Entity
@DiscriminatorValue("Movie") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Movie extends Item {
    private String director;

    private String actor;
}

@Entity
@DiscriminatorValue("Book") // 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정
public class Book extends Item {
    private String author;
    
    private String isbn;
}

@Inheritance(strategy = InheritanceType.SINGLE_TABLE)

- 장점

    - 조인이 필요 없으므로 조회 쿼리가 단순하여 성능이 빠르다

- 단점

    - 자신 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다

    - 단일 테이블에 모든 것을 저장하므로 테이블이 커지기때문에, 상황에따라서는 성능이 좋지 못할 수도 있다

 

구현 클래스 테이블 전략

: 자식 엔티티마다 테이블을 만들어서 사용하는 전략

    - 구분컬럼(@DiscriminatorValue)를 사용하지 않는다.

    - 별로 추천하지 않는 전략이다.

@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(strategy = InheritanceType.TABLE_PER_CLASS)

- 장점

    - subType을 구분해서 처리할 때 좋다

    - not null 제약조건을 사용할 수 있다

- 단점

    - 여러 자식 테이블을 함께 조회할 때 성능이 느리다 (UNION..)

    - 자식테이블을 통합해서 쿼리하기가 까다롭다.

 

@MappedSuperclass

: 부모 클래스를 상속받는 자식 클래스에게 매핑 정보만 제공하고 싶을때 사용한다. (실질적인 Entity가 아님)

@MappedSuperclass // 추상클래스와 비슷
public abstract class CommonEntity {
    @Id
    @GeneratedValue
    private Long id;
    
    private String name;
}

@Entity
@AttributeOverride(name = "id", column = @Column(name = "USER_ID")) // 상속받은 컬럼을 재정의
public class User extends CommonEntity {
    // id상속
    // name상속
    private String phone;
    
    private String email;
}

@Entity
@AttributeOverrides({
    @AttributeOverride(name = "id", column = @Column(name = "PRODUCT_ID")),
    @AttributeOverride(name = "name", column = @Column(name = "PRODUCT_NAME"))
}) // 둘이상 재정의
public class Product extends CommonEntity {
    // id상속
    // name상속
    private Integer price;
    
    private LocalDateTime createDate;
}

공통 속성을 상속받아서 사용하였다. 그리고 상속받은 필드를 재정의하려면 @AttributeOverride를 통해 재정의할 수 있다. (둘이상 재정의하는 경우는 @AttributeOverrides 안에 @AttributeOverride들을 추가)

    - 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할만 한다.

    - @MappedSuperclass로 지정한 클래스는 엔티티가 아니기때문에 find()와 같은 엔티티매니저관련 메소드를 사용할 수 없다.

    - 일반 클래스로는 생성해서 사용하는 일이 거의 없기때문에 추상 클래스(abstract)로 만들어서 쓰는 것을 권장

복합 키와 식별 관계매핑

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

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

    - 필수적 비식별관계(Mandatory): 외래키에 NULL을 허용하지 않는다. 연관관계를 필수로 맺어야함

    - 선택적 비식별관계(Optional): 외래키에 NULL을 허용한다. 연관관계를 맺을지 말지를 선택할 수 있음

 

복합 키: 비식별 관계 매핑

@IdClass

이미 [JPA - 연관관계]편에서 한번 소개한 바가 있다. 다대다 관계를 복합키를 가지는 중간테이블 형태로 사용할 수 있도록 한다. 이 어노테이션은 객체를 복합키형태로 매핑하는 경우에 사용한다.

@Entity
@IdClass(ParentId.class)
public class Parent {
    @Id
    @Column(name = "PARENT_ID1")
    private String id1;
    
    @Id
    @Column(name = "PARENT_ID2")
    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(Object o) {
        return super.equals(o);
    }
    
    @Override
    public int hashCode() {
        return super.hashCode();
    }
}

@Entity
public class Child {
    @Id
    private String id;
    
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1"),
        @JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2")
    })
    // name속성과 동일하면 referencedColumnName속성은 생략가능
    private Parent parent;
}

- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 한다 (id1, id2)

- Serializable 인터페이스를 구현해야 한다

- equals, hashCode를 구현해야 한다

- 기본 생성자가 있어야 한다

- 식별자 클래스는 public이어야 한다

 

@EmbeddedId

: 복합키 관계를 조금 더 객체지향적으로 할 수 있다. @IdClass와는 다르게 식별자 클래스에 기본키를 직접 매핑한다.

@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;
    
    public ParentId() {
    }
    
    public ParentId(String id1, String id2) {
        this.id1 = id1;
        this.id2 = id2;
    }
    
    @Override
    public boolean equals(Object o) {
        return super.equals(o);
    }
    
    @Override
    public int hashCode() {
        return super.hashCode();
    }
}

public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("test_parent");
    EntityManager em = emf.createEntityManager();

    Parent parent = new Parent();
    parentId parentId = new ParendId("myId1", "myId2");
    parent.setId(parentId);
    parent.setName("parent이름");
    em.persist(parent);
    
    ParentId getParentId = new ParentId("myId1", "myId2");
    Parent getParent = em.find(Parent.class, getParentId); // 복합키로 데이터 조회
}

@IdClass와 마찬가지로... @Embeddable도

- Serializable 인터페이스를 구현해야 한다

- equals, hashCode를 구현해야 한다

- 기본 생성자가 있어야 한다

- 식별자 클래스는 public이어야 한다

 

복합 키: 식별 관계 매핑

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

@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, 손자 ID
public class ChildId implements Serializable {
    private String parent; // Child.parent 매핑

    private String childId; // Child.childId 매핑
    // equals, hashCode ..
}

public class GrandChildId implements Serializable {
    private ChildId child; // GrandChild.child 매핑

    private String id; // GrandChild.id 매핑
    // equals, hashCode ..
}

=> @EmbeddedId와 식별 관계

@Entity
public class Parent { // 부모
    @Id
    @Column(name = "PARENT_ID")
    private String id;
    
    private String name;
}

@Entity
public class Child { // 자식
    @MapsId("parentId") // @Id대신 사용. 외래키와 매핑한 연관관계를 기본키에도 매핑하겠다는 뜻. / ChildId.parendId 매핑
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    public Parent parent;
    
    @EmbeddedId
    private ChildId childId;
    
    private String name;
}

@Entity
public class GrandChild { // 손자
    @MapsId("childId") // GrandChildId.childId 매핑
    @ManyToOne
    @JoinColumns({
        @JoinColumn(name = "PARENT_ID"),
        @JoinColumn(name = "CHILD_ID")
    })
    private Child child;
    
    @EmbeddedId
    private GrandChildId id;
    
    private String name;
}

// 자식 ID, 손자 ID
@Embeddable
public class ChildId implements Serializable {
    private String parentId; // @MapsId("parentId")로 매핑

    @Column(name = "CHILD_ID")
    private String id;
    // equals, hashCode ..
}

@Embeddable
public class GrandChildId implements Serializable {
    private ChildId childId; // @MapsId("childId")로 매핑
    
    @Column(name = "GRANDCHILD_ID")
    private String id;
    // equals, hashCode ..
}

위의 식별관계비식별관계로 바꾸어보자.

@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;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "PARENT_ID")
    private 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;
}

- 복합키가 없기때문에 ~Id 클래스를 생성하지 않았다.

 

일대일 식별 관계

 

자식 테이블의 기본 키값으로 부모 테이블의 기본 키 값만 사용한다. 따라서 부모가 복합키가 아니면 자식도 복합키가 아니다.

@Entity
public class Board {
    @Id
    @GeneratedValue
    @Column(name = "BOARD_ID")
    private 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")
    private Board board;
    
    private String content;
}

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

- 데이터베이스 설계 관점에서 보면 비식별 관계를 선호한다. (결국 실제 서비스는 데이터베이스 설계 관점에서 시작하는 경우가 대부분이다)

    - 식별 관계는 부모 테이블에서 자식 테이블로 전파하면서 자식 테이블의 기본키 컬럼이 늘어나는데, 결국 JOIN할때 SQL이 복잡해지고 기본키 인덱스가 불필요하게 커질 수 있다.

    - 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본키를 만들어야 하는 경우가 많다. (+ 별도의 복합 키 클래스를 만들어야함)

    - 식별 관계를 사용할때 의미가 있는 자연키 컬럼을 조합하는 경우가 많고, 비식별 관계는 대리 키를 주로 사용한다. 비즈니스 요구사항 변경이 있는 경우 기본키 변경이 어렵기때문에 대리 키를 사용하는 비식별 관계가 운영보수에 유리하다.

    - 식별 관계는 자식 테이블은 부모 테이블의 기본키를 따라가는 유연하지 못한 구조를 가진다.

    - 식별 관계의 장점으로 자식 테이블에 부모키 정보가 포함되어있으므로 JOIN이 없이도 조회가 가능할 수 있다. (물론 부모의 이름검색등..은 JOIN이 필요해서 한정적이긴 함)

조인테이블

: 데이터베이스 테이블의 연관관계를 설계하는 방법은 크게는 아래의 2가지이다.

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

    - null을 허용해줘야한다

- 조인 테이블 사용 (테이블 사용)

- 객체와 테이블을 매핑할 때 조인 컬럼은 @JoinColumn으로 매핑하고 조인 테이블은 @JoinTable로 매핑한다.

- 조인 테이블은 주로 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 사용

 

일대일 조인 테이블

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

일대다 조인 테이블

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

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

잘 사용하지는 않지만 @SecondaryTable 어노테이션을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있다.

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

- Board 엔티티는 @Table을 사용해서 BOARD 테이블과 매핑, @SecondaryTable을 사용해서 BOARD_DETAIL 테이블을 추가로 매핑했다.

- @SecondaryTable(name): 매핑할 다른 테이블의 이름을 지정

- @SecondaryTable(pkJoinColumns): 매핑할 다른 테이블의 기본키 컬럼 속성을 지정한다

- 더 많은 테이블을 매핑하려면 @SecondaryTables를 사용한다. (안에 @SecondaryTable로 지정)

참고자료

- 서적 - 자바 ORM 표준 JPA 프로그래밍 제 7장 - 김영한 지음

'SpringFramework > JPA' 카테고리의 다른 글

JPA - 객체지향 쿼리 언어, JPQL  (0) 2021.11.17
JPA - 값타입  (0) 2021.11.16
JPA - 연관관계  (0) 2021.11.14
JPA - 엔티티 매핑  (0) 2021.11.12
JPA - 영속성 컨텍스트  (0) 2021.11.03