Spring/jpa

[JPA] 복합키와 식별 관계 매핑

라임온조 2023. 2. 16. 10:59

1. 식별 관계

1) 개념

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

2) 단점

  • 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하면서 자식 테이블의 기본 키 컬럼이 늘어난다. 이 때문에 조인할 때 sql이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있다.
  • 식별 관계는 2개 이상의 컬럼을 합해서 복합 기본 키를 만들어야 하는 경우가 많다
  • 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많다. 그런데, 비즈니스 요구 사항이 시간이 지나면서 변할 수도 있다. 이 경우에 식별 관계의 자연 키 컬럼들이 자식에 손자까지 전파되면 변경이 힘들어지고, 전체적으로 관리도 어려워진다.
  • 부모 테이블의 기본 키를 자식 테이블의 기본 키로 사용해서 비식별 관계보다 테이블 구조가 유연하지 못하다.
  • 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본 키를 사용한다. JPA에서 복합 키는 별도의 복합 키 클래스를 만들어서 사용해야 해서 컬럼이 하나인 기본 키 매핑하는 것보다 많은 노력이 필요하다.

3) 장점

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

2. 비식별 관계

1) 개념

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

2) 분류

필수적 비식별 관계

외래 키에 null을 허용하지 않아서 연관관계를 필수적으로 맺어야 한다

선택적 비식별 관계

외래 키에 null을 허용한다. 연관관계를 맺을지 말지 선택가능.

3) 장점

  • 비식별 관계의 기본 키는 주로 대리 키를 사용하는데 JPA는 @GeneratedValue 처럼 대리 키를 생성하기 위한 편리한 방법을 제공한다.

4) 전략

필수적 비식별 관계

  • 대리 키는 비즈니스와 관련이 없어서 비즈니스가 변경되어도 유연한 대처가 가능하다는 장점이 있다
  • 필수적 관계는 not null로 항상 관계가 있음을 보장해서 내부 조인만 해도 된다. 선택적은 null을 허용해서 조인할 때 외부 조인을 사용해야 한다

기본 키는 Long

  • Integer는 20억 정도면 끝나버리지만 Long은 약 920경이어서 데이터를 많이 저장해도 문제가 생기지 않는다

 

3. 복합 키 : 비식별 관계 매핑

1) @IdClass 사용

PARENT CHILD
PARENT_ID1(PK) CHILD_ID(PK)
PARENT_ID2(PK) PARENT_ID1(FK)
NAME PARENT_ID2(FK)
  NAME
@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){
    }
}
@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;
}

식별자 클래스 조건

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

특징

데베에 맞춘 방법

 

2) @EmbeddedId 사용

@Entity
public class Parent{

	@EmbeddedId
    private ParentId id;
    
    private String name;
    
    ...

}
@Embeddable
public class ParentId implements Serializable{
	@Column(name = "parent_id1")
    private String id;
    
    @Column(name = "parent_id2")
    private String id2;
}

식별자 클래스 조건

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

특징

객체에 맞춘 방법

 

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

1) @IdClass 사용

PARENT CHILD GRANDCHILD
PARENT_ID(PK) PARENT_ID(PK, FK) PARENT_ID(PK,FK)
NAME CHILD_ID(PK) CHILD_ID(PK, FK)
  NAME GRANDCHILD_ID(PK)
    NAME
// 부모
@Entity
public class Parent{
	
    @Id
    @Column(name = "parent_id")
    private String id;
    
    private String name;
}

// 자식
@Entity
@IdClass(Child.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(GrandChild.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;
}
// 자식 식별자 클래스
public class ChildId implements Serializable{
	private String parent;
    private String childId;
}

// 손자 식별자 클래스
public class GrandChildId implements Serializable{
	private ChildId child;
    private String id;
}

2) @EmbeddedId 사용

PARENT CHILD GRANDCHILD
PARENT_ID(PK) PARENT_ID(PK, FK) PARENT_ID(PK,FK)
NAME CHILD_ID(PK) CHILD_ID(PK, FK)
  NAME GRANDCHILD_ID(PK)
    NAME
// 부모
@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")
    @ManyToOne
    @JoinColumn(name = "parent_id")
    public Parent parent;
    
    private String name;
}

// 손자
@Entity
public class GrandChild{
	@EmbeddedId
    private GrandChildId id;
    
    @MapsId("childId")
    @ManyToOne
    @JoinColumns({
    	@JoinColumn(name = "parent_id"),
        @JoinColumn(name = "child_id")
    })
    private String name;
}
// 식별자 클래스

// 자식 
@Embeddable
public class ChildId implements Serializable{
	private String parentId;
    
    @Column(name = "child_id")
    private String id;
}

// 손자
@Embeddable
public class GrandChildId implements Serializable{
	private ChildId childId;
    
    @Column(name = "grandchild_id")
    private String id;
}

3) 비식별 관계로 구현해보기

PARENT CHILD GRANDCHILD
PARENT_ID(PK) CHILD_ID(PK) GRANDCHILD_ID(PK)
NAME PARENT_ID(FK) CHILD_ID(FK)
  NAME NAME
// 부모
@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 = "chlid_id")
    private Child child;
}

5. 일대일 식별 관계

BOARD BOARDDETAIL
BOARD_ID(PK) BOARD_ID(PK, FK)
TITLE CONTENT
// 부모
@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
    @OneToOne
    @JoinColumn(name = "board_id")
    private Board board;
    
    private String content;
}

6. 복합 키와 equals, hashCode()

영속성 컨텍스트는 엔티티의 식별자(보통 id 값)를 키로 사용해서 엔티티를 관리한다. 그리고 식별자를 비교할 때 equals와 hashCode를 사용한다. 따라서 식별자 객체의 동등성(equals 비교, 값이 아예 같은지)이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 영속성 컨텍스트가 엔티티를 관리하는 데 심각한 문제가 발생한다.

따라서 복합 키는 equals와 hashCode를 필수로 구현해서 동일한 키 값을 가졌을 경우 같다는 결과가 나오도록 해야 한다. 이걸 제대로 구현하지 않으면 인스턴스 비교만 해서 정확한 같다는 결과가 나오지 않을 수도 있다.