이번 포스트에서는 복합키에 대해서 정리해볼려고 한다
이전에도 몇번 사용은 해봤지만 헛갈리는 부분도 있고 자주 사용하지 않았다보니 까먹는 부분이 있어 정리해 보고자 한다
Composite Key (복합키)
우선 복합키에 대해서 알아보자
다들 알다시피 DB (여기서 DB는 Oracle, MySQL과 같은 관계형 데이터베이스를 지칭한다) 에서는 주어진 개체 집합 내에서 개체들을 구별할 수 있는 방법이 있어야 한다
다르게 표현하면 한 개체 집합에서 모든 속성들의 값이 정확하게 같은 개체들이 존재하면 안된다는 말이다
이러한 것을 위해서 키의 개념이 필요하고 그 키 중에서는 Super key, Candidate Key, Primary key가 적용 가능하다
이 중에서 우리는 Primary key를 선정해서 개체를 특정할 수 있도록 지정을 하는데 이 key의 구성정보가 2개 이상일 경우 복합키라고 한다
복합키 예시
예를 들어 사용자 정보를 가지고 있는 테이블이 있다고 해보자
해당 테이블에는 사용자이름, 나이, 성별, 생년월일이 있다
User (
username varchar,
age int,
gender varchar,
birthday date
)
가정) 같은 날에 태어난 이름이 같은 사람은 존재하지 않는다
이러한 테이블이 있을 때 과연 어떤 값을 Primary key로 지정을 해야 할 까
- 모든 값을 개별로 사용 할 경우 : 모든 값들은 중복이 가능하다
- username과 birthday를 같이 사용할 경우 : 가정에서 중복 데이터가 존재하지 않는다고 했으므로 유일하게 식별이 가능하게 한다
그러므로 Primary key로는 username
과 birthday
를 합친 값으로 사용이 가능하다
이를 복합키라고 한다
Composite Key with JPA
앞서 복합키에 대해서 알아보았다 이제 이를 JPA로 구현하느 방법을 알아보자
크게 두가지의 방법을 사용할 수 있는데 @EmbeddedId
와 @IdClass
가 있다
@EmbeddedId
부터 알아보자
ER-Diagram
진행 중인 프로젝트에서 간단히 간추려 가져와 보았다
- Post
- 제목과 내용, 카테고리를 가진다
- 카테고리는 여러개를 가질 수 있다
- Category
- 카테고리 명을 가진다
- 여러 Post에 포함이 가능하다
다음과 같이 다대다 관계를 가진 테이블 두 개가 있고 각 Primary key를 Foriegn key이면서 복합키로 사용을 하는 관계 매핑 테이블인 PostCategory테이블이 있다 위 매핑 테이블은 식별 관계로 구성되어있다
짧게 설명을 하면 식별 관계는 부모테이블의 Primary key를 자신의 Primary key + Foriegn key로 사용하는 것이고 비식별 관계는 자신의 별도의 Primary key를 가지며 부모의 Primary key를 Foriegn key로 사용을 하는 형태이다
이 부분에 대해서는 식별관계와 비식별관계해당 블로그를 참고해보자
대 부분 JPA 식별 관계와 비식별 관계에 대해서 찾아보면 비식별 관계로 두고 Primary key를 따로 구성을 하도록 하는 것을 권장하지만 이번 프로젝트에서는 추후 테이블의 변경이 없고 해당 테이블을 가져다가 사용하는 자식 테이블도 생성이 없을 것으로 생각되어 식별관계를 통해 구성을 해봤다
@EmbeddedId
@Entity
public class Post {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
private String title;
private String content;
// 생성자, getter
}
@Entity
public class Category {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
private String name;
// 생성자, getter
}
@Embeddable
@EqualsAndHashCode
public class PostCategoryId implements Serializable {
@Column(name = "POST_ID")
private Long postId;
@Column(name = "CATEGORY_ID")
private Long categoryId;
// 생성자, getter
}
식별관계
@Entity
public class PostCategory implements Persistable<PostCategoryId> {
@EmbeddedId
private PostCategoryId id;
@MapsId("postId") // PostCategoryId.postId 매핑
@ManyToOne
@JoinColumn(name = "POST_ID")
public Post post;
@MapsId("categoryId") // PostCategoryId.categoryId 매핑
@ManyToOne
@JoinColumn(name = "CATEGORY_ID")
public Category category;
@Override
public boolean isNew() {
return true;
}
// 생성자, getter
}
비식별관계
@Entity
public class PostCategory {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "POST_ID", nullable = false)
public Post post;
@ManyToOne
@JoinColumn(name = "CATEGORY_ID", nullable = false)
public Category category;
// 생성자, getter
}
이렇게 보니까 굳이 식별 관계를 안써도 됐을 거 같기도 하고..
Post, Category를 구성을 하는데는 어려움이 없었을 것이다 지금 가장 요점인 PostCategory와 복합키에 대해서 자세히 봐보자
Composite Id class 생성
기본적으로 @EmbeddedId
를 사용을 하기 위해서는 Id 객체를 구성을 해야 하고 해당 객체는 @Embeddable
, equals and hashcode
, 기본 생성자, public 클래스 접근자, Serializable 구현이 필수이다
이 중 @Embeddable
은 Id를 사용하는 쪽에서 @EmbeddedId
를 명시해서 굳이 작성을 하지 않아도 되긴 하지만 복합키라는 것을 알려주기 위해서 명시를 하는 것이 좋아보인다
연관관계 매핑
Id class를 구성하고 해당 Id를 사용하는 Entity에 @EmbeddedId
를 명시를 함으로써 기본적인 세팅은 완료를 했고 연관관계에 대해서 설정을 해주면 된다
기존에 다대일 관계를 매핑할 때 사용하는 @ManyToOne
, @JoinColumn
를 사용하는 것 까지는 같지만 @MapsId
하나를 더 추가를 해 주어야 한다
이는 Foriegn key를 매핑한 연관관계를 Primary key에도 매핑을 하겠다는 의미이고 값으로는 Id class에서 설정해준 Column 이름을 주면 된다
주의
MapsId
만일 @MapsId
를 지정해주지 않으면 Primary key의 Column 명과 연관관계를 위한 Foriegn key의 Column 명이 중복이 되어 org.hibernate.MappingException
이 발생하게 되니 주의하도록 하자
이를 까먹고 꽤 오랜시간동안 삽질을 했다...
implement Persistable
이를 구현하지 않으면 save()
시에 isNew()
가 false가 되어 select query가 우선 실행되게 되므로 필수로 구현을 해주자 isNew()
재정의는 여기선 바로 true가 반환되도록 했지만 다른 방식을 사용하는 것이 좋다
IsNew() 관련 구현 방법 졸작 프로젝트를 하면서 정리한 내용인데 참고해보자
음 다시보니 이미 MapsId에 대해서 작성을 해뒀는데 까먹다니... 이번 기회에 진짜 제대로 체득을 해야겠다
사용 법
Insert
public class PostCategoryService {
private final PostCategoryRepository postCategoryRepository;
private final CategoryRepository categoryRepository;
private final PostRepository postRepository;
public void save(Long postId, Long categoryId) {
Post post = postRepository.getReferenceById(postId);
Category category = categoryRepository.getReferenceById(categoryId);
// id 생성
PostCategoryId id = new PostCategoryId(postId, categoryId);
// id, 연관관계 매핑
postCategoryRepository.save(new PostCategory(id, post, category));
}
}
여기서 한가지 참고할 점은 post와 category 객체를 받아올 때 findById()
를 사용하는 것이 아니라 getReferenceById()
를 사용하는 점이다
이렇게 하게되면 실제 객체가 아닌 프록시 객체를 가져오기 때문에 select query가 나가지 않는다
select
public class PostCategoryService {
private final PostRepository postRepository;
public PostCategory get(Long postId, Long categoryId) {
PostCategoryId id = new PostCategoryId(postId, categoryId);
PostCategory postCategory = postCategoryRepository.findById(id)
.orElseThrow(IllegalArgumentException::new);
return postCategory;
}
}
크게 어려운 부분은 없을 것이다
IdClass
딱히 선호하는 방법은 아니다
Id라는 객체를 사용하기보다는 이러한 아이디가 있어라고 알려주는 것만 하는 것 같다
객체보다는 데이터베이스에 조금 더 가까운 방법같다
@EqualsAndHashCode
public class PostCategoryId implements Serializable {
private Long post;
private Long category;
// 생성자, getter
}
equals and hashcode
, 기본 생성자, public 클래스 접근자, Serializable 구현이 필수이다
@Entity
@IdClass(PostCategoryId.class)
public class PostCategory {
@Id
@ManyToOne
@JoinColumn(name = "POST_ID")// PostCategoryId.post 매핑
private Post post;
@Id
@ManyToOne
@JoinColumn(name = "CATEGORY_ID")// PostCategoryId.category 매핑
private Category category;
// 생성자, getter
}
사용법
Insert
public class PostCategoryService {
private final PostCategoryRepository postCategoryRepository;
public void save(Long postId, Long categoryId) {
Post post = postRepository.getReferenceById(postId);
Category category = categoryRepository.getReferenceById(categoryId);
postCategoryRepository.save(new PostCategoryId(post, category));
}
}
select
public class PostCategoryService {
private final PostCategoryRepository postCategoryRepository;
public PostCategory get(Long postId, Long categoryId) {
PostCategoryId id = new PostCategoryId(postId, categoryId);
PostCategory postCategory = postCategoryRepository.findById(id)
.orElseThrow(IllegalArgumentException::new);
return postCategory;
}
}
추가 - 부모 테이블이 복합키인 자식 테이블 비식별관계 매핑
PostCategory에 연관된 PostCategoryChild가 있다고 생각해보자
@Entity
public class PostCategoryChild {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "POST_ID", nullable = false),
@JoinColumn(name = "CATEGORY_ID", nullable = false)
})
private PostCategory postCategory;
private String childValue;
// 생성자, getter
}
'공부기록 > JPA' 카테고리의 다른 글
JPA Proxy 객체 사용 기 (0) | 2023.01.06 |
---|---|
JPA- JPQL (0) | 2022.01.31 |
JPA-JPQL (0) | 2022.01.28 |
JPA-값타입 (0) | 2022.01.27 |
JPA-프록시 (0) | 2022.01.26 |