해당 글은 김영한 님의 ["자바 ORM 표준 JPA 프로그래밍"] 을 스터디 하면서 정리하는 글 입니다 !👨💻
이번 포스팅에서는 복합키와 비식별 & 식별 관계 매핑에 대해 알아보겠습니다 🧑🏼💻
데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지에 따라 비식별 관계
와 식별 관계
로 나뉩니다.
해당 이미지는 식별 관게를 도식화한 것입니다.
여기서 자식 테이블인 Child
는 부모 테이블의 기본키인 Parent_ID
를 기본키와 외래키로 사용하고 있는 것을 확인할 수 있습니다.
null
을 허용하는지에 따라 필수적 비식별 관계
와 선택적 비식별 관계
로 나뉩니다.해당 이미지는 필수적 비식별 관계
와 선택적 비식별 관계
를 도식화한 것입니다.
여기서 자식 클래스인 Child
는 부모 클래스의 식별자인 Parent_ID
를 외래키로만 사용하는 것을 확인할 수 있습니다.
데이터베이스 테이블을 설계할 때 두 관계 중 하나를 선택해야 하는데 주로 비식별 관계를 사용하고 필요한 경우에만 식별 관계를 사용합니다 👨💻
복합 키(Composite Key)
는 두 개 이상의 컬럼을 Key로 지정하는 것을 말합니다 🙆🏻
PK(Primary Key, 기본키)는 한 테이블에 한 개만 존재할 수 있습니다.
하지만 꼭 한 테이블에 한 컬럼만 기본키로 지정할 수 있는 것은 아니며 여러개의 칼럼을 하나로 묶어 기본 키로 사용할 수 있습니다.
스프링 JPA 에서 둘 이상의 칼럼으로 구성된 복합 기본 키는 별도의 식별자 클래스
를 만들어야 사용할 수 있습니다.
영속성 컨텍스트
에 대해 공부해보았더라면 기본 키(식별자) 를 키로 하여 엔티티를 관리 한다는 것을 알 수 있습니다 👨💻
그리고 기본 키를 구분하기 위해 equals
와 hashCode
를 사용해서 동등성 비교를 합니다. 일반적인 경우인 기본 키가 하나인 경우에는 기본 키의 타입이 자바의 기본 타입을 사용하기에 별도로 두 메소드를 구현할 필요가 없습니다.
하지만 기본 키가 복합 키인 경우에는 식별자 클래스
가 만들어지기에 해당 클래스가 기본 키(복합 키)의 타입이 되며 equals
와 hashCode
를 따로 구현해야 합니다. 이때 필요한 인터페이스가 Serializable
입니다.
public class 식별자 클래스 implements Serializable {
private Long id1;
private Long id2; // 두개의 키가 하나의 복합 키 역할을 함
public 식별자 클래스 () { // 기본 생성자
...
}
public 식별자 클래스(Long id1, Long id2) {
...
}
@Override
public boolean equals(Object o) {..} // equals 메소드 구현
@Override
public int hashCode() {..} // hashCode 메소드 구현
}
지금까지 이야기 한 부분은 식별자 클래스
에 대한 이야기 였습니다.
실제로 스프링 JPA는 이렇게 생성한 식별자 클래스
를 사용하는데 있어 2가지 방식을 제공합니다.
2가지 방식은 뒤에서 구체적으로 알아보겠습니다.
해당 이미지는 복합 키를 가지는 부모 클래스인 Parent
클래스와 부모 클래스와 비식별 관계를 맺는 Child
클래스에 대한 이미지입니다.
앞에서 이야기 했던 것처럼 스프링 JPA 는 복합키를 지원하기 위해 2가지 방식을 제공합니다.
// 복합키를 가지는 부모 클래스(Parent) 에 대한 식별자 클래스
public class ParentId implements Serializable {
private String id1; // parent.id1 매핑
private String id2; // parent.id2 매핑
public ParentId() {
}
@Override
public boolean equals(Object o) {..}
@Override
public int hashCode() {..}
}
@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;
...
}
해당 코드를 통해 @IdClass
를 사용해서 식별자 클래스
를 활용하는 것을 확인할 수 있습니다 🧑🏼💻
@IdClass
를 사용할 때 식별자 클래스는 다음 조건을 만족해야 합니다.
식별자 클래스
의 속성명과 복합키를 가지는 엔티티에서 사용하는 식별자의 속성명이 같아야 합니다.Serializable
인터페이스를 구현해야 합니다.equals
,hashCode
를 구현해야 합니다.@IdClass
를 활용하여 복합 키를 가지는 엔티티를 저장 시 영속성 컨텍스트에서 엔티티를 등록하기 직전에 식별자 클래스를 생성하고 키로 사용합니다.
// 복합 키를 가지는 엔티티 저장하는 과정
Parent p = new Parent();
p.setId1("myId1");
p.setId2("myId2"); // 자동으로 식별자 클래스 객체 생성
p.setName("myName");
em.persist(p);
// 복합 키를 가지는 엔티티 조회하는 과정
ParentId parentId = new ParentId("myId1", "myId2");
Parent parent = em.find(Parent.class, parendId); // 식별자 클래스를 식별자로 설정
이번에는 복합 키를 가지는 부모 클래스인 Parent
와 비식별 관계
를 가지는 자식 클래스에 대해 알아보겠습니다.
@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;
}
앞에서 살펴본바와 같이 비식별 관계
는 자식 클래스가 부모 클래스의 기본 키를 외래 키로만 가지는 관계입니다.
이와 같은 경우는 부모 클래스가 복합 키를 가짐으로 자식 클래스의 외래 키도 복합 키가 됩니다.
따라서 @JoinColumns
를 활용하여 외래 키가 복합 키인것을 나타냈습니다 🧑🏼💻
이번에는 @EmbeddedId
를 활용한 방법을 알아보겠습니다.
위에서 살펴본 @IdClass
가 데이터베이스에 맞춘 방법이라면 EmbeddedId
는 좀 더 객체지향적인 방법입니다.
// 복합키를 가지는 부모 클래스(Parent) 에 대한 식별자 클래스
@Embeddable // 식별자 클래스에 꼭 추가해야하는 어노테이션
public class ParentId implements Serializable {
@Column(name = "PARENT_ID1")
private String id1; // parent.id1 매핑
@Column(name = "PARENT_ID2")
private String id2; // parent.id2 매핑
// equals,hashCode 구현..
}
@Entity
public class Parent {
@EmbeddedId
private ParentId id; // 식별자 클래스를 직접 사용
Private String name;
...
}
@IdClass
와는 달리 @EmbeddedId
를 적용한 식별자 클래스는 식별자 클래스에 기본 키를 직접 매핑합니다.
또한 앞서 @Idclass
에서 식별자 클래스에 5가지 조건 중 1번에 해당 하는 조건이 @Embeddable
어노테이션을 붙어주어야 한다로 변경됩니다 🧑🏼💻
// 복합 키를 가지는 엔티티 저장하는 과정
Parent p = new Parent();
ParentId parentId = new ParentId("myId1", "myId2");
p.setId(parentId);
p.setName("parentName");
em.persist(p);
@IdClass
에서 저장하는 방법과 달리 직접 식별자 클래스 객체를 생성합니다.
// 복합 키를 가지는 엔티티 조회하는 과정
ParentId parentId = new ParentId("myId1", "myId2");
Parent p = em.finf(Parent.class, parentId); // 식별자 클래스를 식별자로 설정
조회하는 방식은 IdClass
에서 조회하는 방식과 동일합니다.
해당 이미지는 Parent
클래스와 식별 관계
를 맺는 Child
클래스의 이미지입니다.
비식별 관계
와 마찬가지로 매핑 방법에는 @IdClass, @EmbeddedId 가 있습니다.
// 식별 관계를 맺는 부모 클래스
@Entity
public class Parent {
@Id
@Column(name = "PARENT_ID")
private String id;
private String name;
}
// 식별 관계를 맺는 자식 클래스의 식별자 클래스
public class ChildId implements Serializable {
private String parent;
private String childId;
// 기본 생성자, equals, hashCode
...
}
// 식별 관계를 맺는 자식 클래스
@Entity
public class Child {
@Id
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent; // 식별자와 연관관계 동시 매핑
@Id
private String childId;
private String name;
....
}
@IdClass
를 사용하는 식별 관계 매핑은 기본 키와 외래 키를 같이 매핑해야합니다.
따라서 식별자 매핑인 @Id
와 연관관계 매핑인 @ManyToOne
을 같이 사용합니다.
// 식별 관계를 맺는 부모 클래스
@Entity
public class Parent {
@Id
@Column(name = "PARENT_ID")
private String id;
private String name;
}
// 식별 관계를 맺는 자식 클래스의 식별자 클래스
@Embeddable
public class ChildId implements Serializable {
private String parentId; // @MapsId를 사용했기 때문에 매핑할 필요 없음
@Column(name = "CHILD_ID")
private String id;
// 기본 생성자, equals, hashCode
...
}
// 식별 관계를 맺는 자식 클래스
@Entity
public class Child {
@EmbeddedId
private ChildId id; // 식별자 클래스를 직접 사용
@MapsId("parentId") // 외래키와 매핑한 연관관계를 기본 키에도 매핑하기 위한 용도
@ManyToOne
@JoinColumn(name = "PARENT_ID")
public Parent parent;
...
}
@MapsId
라는 어노테이션을 주의깊게 봅시다 🤔
@MapsId
는 외래키와 매핑한 연관관계를 기본 키에도 매핑하겠다는 뜻입니다.
해당 어노테이션의 속성 값은 @EmbeddedId 를 사용한 식별자 클래스의 기본 키 필드를 지정하면 됩니다.
데이터베이스 설계 관점에서 보면 식별 관계보다는 비식별 관계를 선호합니다 👍
@GeneratedValue
와 같이 대리 키를 생성하는 편리한 방법을 제공합니다.하지만 식별 관계에도 상황에 따른 장점은 존재합니다.
먼저 기본 키 인덱스를 사용하기 편리하며 상위 테이블의 기본 키 칼럼을 자식 테이블들이 가지고 있으므로 특정 상황에 조인이 필요가 없습니다.
결론은 가능한 비식별 관계를 사용하고 기본 키는 Long 타입의 대리 키를 사용하는 것입니다
또한 비식별 관계에서도 Null 값을 허용하지 않는 필수적 비식별 관계를 사용하는 것이 좋은데 이는 내부 조인을 사용할 수 있기 때문입니다. ❗️❗️