개요
복합 기본키(Composite Primary Key)란 두 개 이상의 컬럼을 묶어 만든 기본키(Primary Key)를 말한다.
복합키는 다대다(N:M) 관계의 테이블의 관계를 풀어주고 공통으로 저장되는 데이터를 다룰 때 사용되는 매핑 테이블에서 사용될 수 있다.
예를 들어, 내가 현재 개발중인 업무평가시스템에서 하나의 팀은 다양한 업무를 담당할 수 있고 하나의 업무를 여러 팀이 담당할 수 있다.
즉, 팀과 업무 테이블은 다대다 관계를 갖고 있다. 그리고 각 업무에 대한 팀별로 평가를 받을 수 있다. 이 때 매핑 테이블 구조는 다음과 같다. 이제 Spring Data JPA에서 외래키로 복합키를 만드는 방법을 알아보자
- taskId: 업무 테이블의 외래키(foreign key)
- teamId: 팀 테이블의 외래키(foreign key)
- point: 해당 업무에 대한 담당 팀 평가 점수
개발 환경
- Java 8
- Spring Boot 2.7.x
- Gradle
- JPA, Hibernate
구현
복합키로 사용할 외래키를 가진 Entity
우선 Task(업무), Team(팀)의 Entity 클래스는 다음과 같다.
현재 프로젝트에서는 Task나 Team에서 TaskEvaluation을 조회할 일이 없었기 때문에 단방향 ManyToOne으로 관계를 맺기 때문에 TaskEvaluation을 참조하고 있지 않다.
@Getter
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@ToString
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "TASK")
public class TaskEntity extends BaseEntity {
// 아이디
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
// 년도
@Column(length = 4, nullable = false)
@Size(min = 4, max = 4)
private String year;
// 프로젝트명
@Column(length = 50)
@Size(min = 1, max = 50)
private String projectTitle;
// 업무명
@Column(length = 300, nullable = false)
@Size(min = 1, max = 300)
private String taskTitle;
// 업무상태
@Column(length = 10)
@Size(min = 1, max = 10)
private String taskState;
// 진척도
@Column(length = 10)
@NotNull
private int taskProgress;
}
@Getter
@SuperBuilder
@EqualsAndHashCode(callSuper = true)
@ToString
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "TEAM")
public class TeamEntity extends BaseEntity {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
// 팀명
@Column(length = 50, nullable = false)
private String title;
}
외래키를 복합 기본키로 사용하기
이제 매핑 테이블 역할을 할 TeamEvaluation Entity 클래스를 만들어보자. TeamEvaluation Entity는 팀의 업무 평가 정보를 저장한다.
JPA에서 복합 기본키를 사용하기 위해서는 별도 클래스를 만들어야 한다. 복합 기본키 역할을 할 클래스는 다음 조건을 충족해야 한다.
- 기본 생성자가 있어야 한다.
- Serializeable 인터페이스를 구현해야 한다.
- equals() 메서드와 hash() 메서드를 재정의해야 한다.
- @Embeddable 애너테이션을 붙여야 한다.
위 조건을 만족하는 클래스를 만들면 아래와 같다. 클래스 이름은 TaskEvaluationEntityId로 지었다.
@Embeddable
public class TaskEvaluationEntityId implements Serializable {
@Column(name = "task_id")
private Long taskId;
@Column(name = "charge_team_id")
private Long chargeTeamId;
public TaskEvaluationEntityId() {
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TaskEvaluationEntityId that = (TaskEvaluationEntityId) o;
return Objects.equals(taskId, that.taskId) && Objects.equals(chargeTeamId,
that.chargeTeamId);
}
@Override
public int hashCode() {
return Objects.hash(taskId, chargeTeamId);
}
}
위 클래스는 조건을 만족하기는 하지만 보일러 플레이트 코드때문에 쓸데없이 코드가 길다.
그래서 보일러 플레이트 코드를 생성해주는 Lombok을 사용했다.
@Embeddable
@EqualsAndHashCode
@NoArgsConstructor
public class TaskEvaluationEntityId implements Serializable {
@Column(name = "task_id")
private Long taskId;
@Column(name = "charge_team_id")
private Long chargeTeamId;
}
- @Embeddable: 해당 클래스가 엔티티에 포함될 수 있음을 JPA에게 알려주는 애너테이션. 이 애너테이션이 붙은 클래스의 필드는 Entity의 DB 테이블에 매핑된다.
- @EqualsAndHashCode: Equals 메서드와 HashCode 메서드를 재정의해준다.
- @NoArgsConstructor: 기본 생성자를 생성해준다.
이제 TeamEvaluation Entity 클래스를 만들어보자
@SuperBuilder
@Getter
@EqualsAndHashCode(callSuper = true)
@Entity(name = "TASK_EVALUATION")
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TaskEvaluationEntity extends BaseEntity {
@EmbeddedId
private TaskEvaluationEntityId id;
@MapsId("taskId")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "task_id")
private TaskEntity task;
@MapsId("chargeTeamId")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "charge_team_id")
private TeamEntity team;
@Column(precision = 10, scale = 2)
private double point; // 평가 점수
}
- @EmbeddedId: 복합 기본 키라는 것을 알려준다. 해당 클래스에는 @Embeddable 애너테이션이 붙어있어야 한다.
- @MapsId: EmbeddedId와 함께 사용하며 Entity 객체를 Embeddable 클래스의 필드에 연결해주는 역할을 한다.
- @ManyToOne: TaskEvaluation는 Task와 Team에 대해 다대일(N:1) 관계를 갖고 있기 때문에 @ManyToOne을 사용했다.
테스트
application.yml(또는 application.properties)에 아래 설정을 추가해주고 애플리케이션을 실행해서 의도한대로 복합 기본키를 가진 테이블 생성되는지 확인해보자
# JPA 설정 정보
spring:
jpa:
hibernate:
ddl-auto: create-drop
defer-datasource-initialization: true
show-sql: true # true 설정 시, 콘솔에 JPA 쿼리 출력
properties:
hibernate:
format_sql: true # 콘솔에 표시되는 쿼리를 가독성있게 출력
task_evaluation 테이블을 생성하는 쿼리를 보면 primary key (charge_team_id, task_id)로 참조키를 복합 기본키로 사용하는 것을 알 수 있다.
성공!
Hibernate:
# team table 생성
create table team (
id bigint generated by default as identity,
title varchar(50) not null,
primary key (id)
)
# task table 생성
create table task (
id bigint generated by default as identity,
project_title varchar(50),
task_progress integer not null,
task_state varchar(10),
task_title varchar(300) not null,
year varchar(4) not null,
primary key (id)
)
# task_evaluation table 생성. 참조키를 복합키로 사용하고 있다.
create table task_evaluation (
charge_team_id bigint not null,
task_id bigint not null,
point double,
primary key (charge_team_id, task_id)
)
alter table task_evaluation
add constraint FK7xru9ioepwekfal3hybwk83l0
foreign key (charge_team_id)
references team
alter table task_evaluation
add constraint FK8vktactaa3ko3hu0yur1sbsmd
foreign key (task_id)
references task
참조
https://www.geeksforgeeks.org/can-a-composite-key-be-a-foreign-key-in-another-table/
https://samuel-mumo.medium.com/spring-data-jpa-composite-key-mapping-example-750eb54a3d99
https://www.digitalocean.com/community/tutorials/hibernate-many-to-many-mapping-join-tables
'JPA' 카테고리의 다른 글
JPA - 하나의 컬럼에 여러 개의 데이터를 저장하기 (0) | 2023.05.08 |
---|---|
JPA - Querydsl를 사용해 DTO 받는 방법 (0) | 2023.03.02 |
Spring Data JPA - Auditing으로 생성일, 수정일 자동화하는 법 (0) | 2022.12.02 |