식별관계와 비식별 관계
DB 테이블 사이 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분합니다.
식별 관계
부모 테이블의 기본 키를 내려 받아 자식 테이블의 기본 키 + 외래 키로 사용하는 관계입니다.
비식별 관계
부모 테이블의 기본 키를 받아 자식 테이블의 외래 키로만 사용하는 관계입니다.
필수적 비식별 관계
PARENT - PARENT_ID (PK) / NAME
CHILD - CHILD_ID (PK) / PARENT_ID (FK) / NAME
선택적 비식별 관계
PARENT - PARENT_ID (PK) / NAME
CHILD - CHILD_ID (PK) / PARENT_ID (FK) / NAME
비식별 관계는 PARENT 테이블의 기본 키를 받아 CHILD 테이블의 외래키(FK)로만 사용하고 기본 키(PK)로는 사용하지 않는 모습을 확인할 수 있습니다. 비식별 관계는 외래 키에 NULL 허용하는지에 따라 필수적 비식별 관계와 선택적 비식별 관계로 구분됩니다.
테이블 설계 시 식별 관계나 비식별 관계 중 하나를 선택해야 하는데 최근에는 비식별 관계를 주로 사용하고 반드시 필요한 곳에만 식별 관계를 사용하는 것이 일반적입니다.
기본 키를 구성하는 컬럼이 하나면 단순하게 매핑합니다.
@Entity
public class Hello {
@Id
private String id;
}
@Entity // 복합키 @Id 사용시 오류 발생, 둘 이상은 별도 식별자 클래스를 만들어야 합니다
public class Hello {
@Id
private String id1;
@Id
private String id2;
}
JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티 식별자를 키로 사용합니다. 그리고 식별자를 구분하기 위해서 equals와 hashCode를 사용해서 동등성 비교를 합니다. 하나의 식별자 필드만 있을 때는 보통 자바 기본 타입을 사용해서 문제가 없지만 식별자 필드가 2개 이상이면 별도 식별자 클래스를 만들고 그 곳에 eqauls와 hashCode를 구현해야 합니다.
JPA는 복합키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공하고 있습니다. @IdClass는 관계형 데이터베이스에 가까운 방법이고 @EmbeddedId는 좀 더 객체지향에 가까운 방법입니다.
@IdClass
둘 이상은 별도의 식별자 클래스 @IdClass를 사용해야 합니다.
복합 키 테이블은 비식별 관계이고 PARENT는 복합 기본 키를 사용합니다.
복합 키 테이블
PARENT - PARENT_ID1 (PK) / PARENT_ID2 (PK) / NAME
CHILD - CHILD_ID (PK) / PARENT_ID1 (FK) / 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;
}
PARENT 테이블을 보면 기본 키를 PARENT_ID1, PARENT_ID2 로 묶은 복합 키로 구성했습니다. 그래서 복합 키를 매핑하기 위해 식별자 클래스를 별도로 만들어야 합니다.
먼저 Parent 클래스는 기본 키 컬럼을 @Id로 매핑합니다. 그리고 @IdClass를 사용해서 ParentId 클래스처럼 @IdClass 를 사용해서 ParentId 클래스를 식별자 클래스로 지정했습니다.
// 식별자 클래스
private 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 eqauls(Object o) { ... }
@Override
public int hashCode() { ... }
}
@IdClass 클래스 사용시 충족해야하는 조건
- 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 합니다.
(위 코드처럼 Parent.id1 과 ParentId.id1 그리고 Parent.id2 와 ParentId.id2 가 같은 것처럼)
- Serializable 인터페이스를 구현해야 합니다.
- eqauls, hashCode를 구현해야 합니다.
- 기본 생성자가 있어야 합니다.
- 식별자 클래스는 public 이어야 합니다.
코드에서는 식별자 클래스 ParentId가 보이지 않지만 em.persist()를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용합니다.
조회 코드를 보면 식별자 클래스인 ParentId를 사용해서 엔티티를 조회합니다.
@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;
Parent 테이블의 기본 키 컬럼이 복합 키이므로 Child 테이블의 외래키도 복합키 입니다. 그래서 외래 키 매핑 시 여러 컬럼을 매핑해야 하므로 @JoinColumns 어노테이션을 사용하고 각각 외래 키 컬럼을 @JoinColumn으로 매핑합니다.
추가적으로 예제처럼 @JoinColumnName의 name 속성과 referenceColumnName 속성이 같으면 referenceColumnName 생략이 가능합니다.
@EmbeddedId
@Entity
public class Parent {
@EmbeddedId
private ParentId id;
private String name;
...
}
Parent 엔티티에서 식별자 클래스를 직접 사용하고 @EmbeddedId 어노테이션을 작성하면 됩니다.
@Embeddable
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1;
@Column(name = "PARENT_ID2")
private String id2;
// @Override eqauls, hashCode 구현
...
}
식별자 관계 코드입니다.
@IdClass 와 다른 점은 @EmbeddedId를 사용한 식별자 클래스는 식별자 클래스에 기본키를 직접 매핑합니다.
@EmbeddedId 사용한 클래스가 충족해야하는 조건
- @Embeddable 어노테이션을 붙여줘야 합니다
- Serializable 인터페이스를 구현해야 합니다.
- equals, hashCode를 구현해야 합니다.
- 기본 생성자가 있어야 합니다.
- 식별자 클래스는 public 이어야 합니다.
@EmbeddedId를 사용하는 코드
Parent parent = new Parent();
ParentId parentId = new ParentId("myId1", "myId2"); // 식별자 클래스 parentId를 직접 사용
parent.setId(parentId);
parent.setName("parentName");
em.persist(parent);
저장하는 코드를 보면 식별자 클래스 parentId를 직접 생성해서 사용합니다. 엔티티를 조회해보면
ParentId parentId = new ParentId("myId1", "myId2"); // 식별지 클래스 parentId를 직접 사용
Parent parent = em.find(Parent.class, parentId);
복합 키와 eqauls(), hashCode()
ParentId id1 = new ParentId();
id1.setId1("myId1");
id1.setId2("myId2");
ParentId id2 = new ParentId();
id2.setId1("myId1");
id2.setId2("myId2");
id1.eqauls(id) -> ?
id1, id2는 같은 값을 갖고 있지만 인스턴스가 다릅니다.
eqauls()를 적절히 오버라이딩 하면 참이지만 eqauls()를 적절히 오버라이딩 하지 않았다면 결과는 거짓입니다. 그 이유는 자바의 모든 클래스는 기본으로 Object 클래스를 상속받는데 이 클래스가 제공하는 기본 eqauls()는 인스턴스 참조 값 비교인 == 비교(동일성 비교)를 하기 때문입니다.
영속성 컨텍스트는 엔티티 식별자를 키로 사용해서 엔티티를 관리합니다. 식별자를 비교할 때 equals()와 hashCode()를 사용합니다. 그래서 식별자 객체의 동등성(equlas 비교)이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 문제가 발생합니다. 그래서 equals(), hashCode()를 필수로 구현해야 합니다.
@IdClass와 @EmbeddedId
@EmbededdId가 @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를 사용할 수 없다
복합 키 식별 관계 매핑
PARENT - PARENT_ID (PK) / NAME
CHILD - PARENT_ID (PK, FK) / CHILD_ID (PK)
GRANDCHILD - PARENT_ID (PK, FK) / CHILD_ID (PK, FK) / GRANDCHILD_ID (PK) / NAME
@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;
...
}
// 자식
public class ChildId implements Serializable {
private String parent; // Child.parent 매핑
private String childId; // Child.childId 매핑
// eqauls, hashCode
...
}
// 손자
@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
@Id
@ManyToOne
@JoinColumn({
@JoinColumn(name = "PARENT_ID")
@JoinColumn(name = "CHILD_ID")
})
private Child child;
@Id @Column(name = "GRANDCHILD_ID")
private String id;
private name;
...
}
// 손자 ID
public class GrandChildId implements Serializable {
private ChildId child; // GrandChild.child 매핑
private String id; // GrandChild.id 매핑
// eqauls, hashCode
...
}
@IdClass 사용방법. 식별 관계는 기본키와 외래키를 같이 매핑해야 합니다. 식별자 매핑인 @Id와 연관관계 매핑인 @ManyToOne 같이 사용하면 됩니다.
Child 엔티티의 parent 필드를 보면 @Id 기본 키를 매핑하면서 @ManyToOne과 @JoinColumn으로 외래 키를 같이 매핑합니다.
@EmbeddedId와 식별관계
@EmbeddedId로 식별 관계를 구성할 때는 @MapsId를 사용해야 합니다
// 부모
@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") // @MapsId는 외래키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 의미
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
private String name;
}
// 자식 Id
@Embeddable
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("childId") // GrandChildId.childId 매핑
@ManyToOne
@JoinColumn({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
private String name;
...
}
// 손자
@Entity
public class GrandChild {
@EmbeddedId
private GrandChildId id;
@MapsId("childId") // GrandChildId.childId 매핑
@ManyToOne
@JoinColumn({
@JoinColumn(name = "PARENT_ID"),
@JoinColumn(name = "CHILD_ID")
})
private Child child;
private String name;
...
}
// 손자 ID
@Embeddable
public class GrandChildId implement Serializable {
private ChildId childId; // @MapsId("childId")로 매핑
@Column(name = "GRANDCHILD_ID")
private String id;
// eqauls, hashCode
...
}
@EmbeddedId는 식별 관계로 사용할 연관관계 속성에 @MapsId를 사용하면 됩니다. @IdClass와 다른 점은 @Id 대신 @MapsId를 사용한다는 점입니다. @MapsId 속성 값은 @EmbeddedId 사용한 식별자 클래스의 기본 키 필드를 지정하면 됩니다. 위 코드에서는 ChildId의 parentId를 선택했습니다.
비식별 관계 구현
// 부모
@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;
...
}
식별 관계 코드와 비교하면 매핑도 쉽고 코드도 단순하며 복합 키 클래스도 필요하지 않습니다.
일대일 식별 관계
// 부모
@Entity
public class Board {
@Id @GeneratedValue
@Column(name = "BOARD_ID")
private Long id;
private String title;
@OneToOne(mappedBy = "board")
private BoardDetaial boardDetail;
...
}
// 자식
@Entity
public class BoardDetail {
@Id
private Long boardId;
@MapsId
@OneToOne
@JoinColumn(name = "BOARD_ID"0
private Board board;
private String content;
...
}
자식 테이블의 기본 키 값으로 부모 테이블의 기본 키값만 사용합니다. 그래서 부모 테이블의 기본 키가 복합 키가 아니면 자식 테이블의 기본키는 복합키로 구성하지 않아도 됩니다.
식별, 비식별 관계 장단점
DB 설계 관점에서는 아래 이유들로 비식별 관계를 선호합니다.
- 식별 관계는 부모 테이블의 기본 키를 자식 테이블로 전파하며 자식 테이블의 기본 키 컬럼이 늘어납니다. 부모 테이블 기본키는 1개지만 자식은 2개, 손자 테이블은 기본 키 컬럼이 3개로 늘어납니다. 조인할 때 SQL이 복잡해지고 기본 키 인덱스가 불필요하게 커질 수 있습니다.
- 식별관계는 2개 이상의 컬럼을 합해 복합 기본키를 만들어야 합니다.
- 식별 관계를 사용할 때 기본 키로 비즈니스 의미가 있는 자연 키 컬럼을 조합하는 경우가 많습니다. 그래서 비즈니스 요구사항 변경에 불리하게 됩니다. 반면 비식별 관계 기본 키는 비즈니스와 전혀 관계없는 대리 키를 주로 사용합니다.
- 식별 관계는 부모 테이블 기본 키를 자식 테이블의 기본 키로 사용하여 테이블 구조가 유연하지 모합니다.
객체 관계 매핑 관점에서 비식별 관계를 선호하는 이유
- 일대일 관계를 제외하고 식별 관계는 2개 이상의 컬럼을 묶은 복합 기본 키를 사용해야 합니다. 별도의 복합 키 클래스를 만들어 사용해야 합니다. 컬럼이 하나인 기본키를 매핑하는 것보다 많은 노력이 필요하게 됩ㅂ니다.
- 비식별 관계의 기본 키는 주로 대리 키를 사용하며 JPA는 @GenerateValue 처럼 대리 키 생성을 위한 편한 방법을 제공합니다.
식별 관계 장점도 있습니다.
기본 키 인덱스를 활요하기 좋습니다. 특정 상황에 조인 없이 하위 테이블 만으로 검색을 완료할 수 있습니다. 비식별 관계를 사용하고 기본 키는 Long 타입의 대리키를 사용하는 것도 방법입니다. 선택적 비식별 관계보다는 필수적 비식별 관계를 사용하는 것이 좋은데 선택적 비별 관계는 NULL을 허용하기에 조인할 때 외부 조인을 사용해야 합니다. 반면 필수적 관계는 NOT NULL로 항상 관계가 있다는 것을 보장해서 내부 조인만 사용해도 됩니다.
함수형 인터페이스 (Operator, Predicate), andThen(), compose() (1) | 2024.02.06 |
---|---|
메서드 참조와 생성자 참조 (0) | 2024.02.05 |
함수 인터페이스 (Consumer, Supplier, Function) (1) | 2024.02.03 |
람다식 사용법 (0) | 2024.02.02 |
제네릭 정리 (0) | 2024.01.31 |
댓글 영역