복합키(Composite key)
관계형 데이터베이스에서 복합키(Composite Key)란, 두 개 이상의 컬럼을 조합하여 하나의 기본 키(Primary Key)로 사용하는 것을 의미한다. 단일 컬럼만으로는 데이터의 고유성을 보장할 수 없을 때, 여러 컬럼을 묶어 하나의 키로 사용하는 방식이다.
아래 테이블처럼, 주문번호와 상품번호 두개를 묶어야 하나의 고유한 key가 되는 경우를 말한다.
이런 경우에 JPA에서 단일키를 사용할 때보다 복잡하고, 귀찮은 작업이 필요하다.
복합키가 꼭 필요하지 않다면, 단일키 전략을 사용하는 것이 낫다고 생각하지만,
이미 존재하는 테이블을 엔티티 매핑 해야할 때가 있을것이다.
복합키 매핑 시 특징
- key class에 Serializable을 구현해야된다.
- key class에 equals() , hashCode()를 구현해야된다.
- 복합키 또는 복합키 중 일부에는 @GenerateValue로 키를 자동생성 할 수 없다.
- @IdClass, @Id, @MapsId, @EmbeddedId 등의 어노테이션을 사용한다.
예제 테이블 order
아래 테이블은 order_id, product_id를 복합키로 가지고, customer_id를 FK로 가진다.
create table order_record
(
order_id integer not null,
product_id integer not null,
customer_id integer not null,
quantity integer,
order_date timestamp default current_timestamp,
primary key (order_id, product_id),
constraint fk_order_customer
foreign key (customer_id) references customer(id)
);
@IdClass
@IdClass 어노테이션을 이용하면, Entity 클래스에서 id 필드들에 직접 접근할 수 있다는 장점이 있다.
복합키 class
import java.io.Serializable;
import java.util.Objects;
// @Getter, @AllArgsConstructor, @NoArgsConstructor
@EqualsAndHashCode
public class OrderRecordId implements Serializable {
private Integer orderId;
private Integer productId;
}
Entity class
위의 복합키 클래스를 @IdClass에 명시해주고, 복합키 필드를 엔티티 클래스에도 만들고 각각 @Id 어노테이션을 붙인다.
@IdClass(OrderRecordId.class)
@Table(name = "order_record")
@Entity
public class OrderRecord {
@Id
@Column(name = "order_id")
private Integer orderId;
@Id
@Column(name = "product_id")
private Integer productId;
@ManyToOne
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
private Integer quantity;
@Column(name = "order_date")
private LocalDateTime orderDate;
}
Repository class
ID로는 OrderRecordId를 지정한다.
@Repository
public interface OrderRecordRepository extends JpaRepository<OrderRecord, OrderRecordId> {
}
저장
저장시에는 복합키의 필드들에 직접 값을 할당해야한다.
@Transactional
public void saveOrderRecord(Integer orderId, Integer productId) {
Customer customer = customerRepository.findById(1L).orElseThrow();
// OrderRecord 객체 생성
OrderRecord order = OrderRecord.builder()
.orderId(1001)
.productId(2001)
.customer(customer)
.quantity(3)
.orderDate(LocalDateTime.now())
.build();
// 저장
orderRecordRepository.save(order);
}
조회
JPA Repository
findById로 조회할 때는 복합키 클래스를 생성해서 사용해아한다.
물론, findByOrderIdAndProductId()와 같이 사용해도 된다.
public Optional<OrderRecord> getOrderRecord(Integer orderId, Integer productId) {
OrderRecordId id = new OrderRecordId(orderId, productId);
return orderRecordRepository.findById(id);
}
QueryDSL
QueryDSL에서는 각 복합키의 요소들을 개별 필드로 접근한다.
public Optional<OrderRecord> findById(Integer orderId, Integer productId) {
QOrderRecord orderRecord = QOrderRecord.orderRecord;
OrderRecord orderRecord = queryFactory
.selectFrom(orderRecord)
.where(
orderRecord.orderId.eq(orderId),
orderRecord.productId.eq(productId)
)
.fetchOne();
return Optional.ofNullable(orderRecord);
}
@EmbeddedId
@IdClass를 이용하는 경우엔, 이 필드들이 복합키 필드들이라는 것을 JPA에게 인식시키는 정도였다면,
@EmbeddedId를 사용하는 경우엔, 복합키 클래스를 객체로 다루며 조금 더 객체지향에 가까운 방식이라는 생각이 든다.
그러나.. @IdClass가 조금 더 직관적이고 사용하기 편했다...
복합키 class - @Embeddable
@IdClass 방식과 동일하게 Serializabled을 구현하지만, @Embeddable 어노테이션을 추가해야한다.
import java.io.Serializable;
// @Getter, @AllArgsConstructor, @NoArgsConstructor
@EqualsAndHashCode
@Embeddable
public class OrderRecordId implements Serializable {
@Column(name = "order_id")
private Integer orderId;
@Column(name = "product_id")
private Integer productId;
}
Entity class
@Entity
@Table(name = "order_record")
public class OrderRecord {
@EmbeddedId
private OrderRecordId id;
@ManyToOne
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
private Integer quantity;
@Column(name = "order_date")
private LocalDateTime orderDate;
}
Repository class
Repository class는 동일하다.
@Repository
public interface OrderRecordRepository extends JpaRepository<OrderRecord, OrderRecordId> {
}
저장
저장시에는 복합키를 생성해서 할당한다.
@Transactional
public void save() {
Customer customer = customerRepository.findById(1L).orElseThrow();
OrderRecordId orderRecordKey = new OrderRecordId(1001, 2002);
// 객체 생성
OrderRecord order = new OrderRecord();
order.setId(orderRecordKey);
order.setCustomer(customer);
order.setQuantity(2);
order.setOrderDate(LocalDateTime.now());
// 저장
orderRecordRepository.save(order);
}
조회
JPA Repository
위와 동일하다.
public Optional<OrderRecord> findById(Integer orderId, Integer productId) {
OrderRecordId id = new OrderRecordId(orderId, productId);
return orderRecordRepository.findById(id)
.orElseThrow(() -> new RuntimeException("Not found"));
}
QueryDSL
public Optional<OrderRecord> findUsingQueryDsl(Integer orderId, Integer productId) {
QOrderRecord orderRecord = QOrderRecord.orderRecord;
OrderRecord entity = queryFactory.selectFrom(orderRecord)
.where(orderRecord.id.orderId.eq(orderId)
.and(orderRecord.id.productId.eq(productId)))
.fetchOne();
return Optional.ofNullable(entity);
}
연관관계 매핑
@ManyToOne 매핑
OrderRecord를 부모로 가지는 @ManyToOne 매핑이 된 Delivery 클래스 예제이다.
@JoinColumns를 이용해서 두개의 복합키를 모두 매핑해야한다.
@Entity
@Table(name = "delivery")
public class Delivery {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String address;
private LocalDateTime shippedAt;
@ManyToOne
@JoinColumns({
@JoinColumn(name = "order_id", referencedColumnName = "order_id"),
@JoinColumn(name = "product_id", referencedColumnName = "product_id")
})
private OrderRecord orderRecord;
}
양방향 매핑 추가
@IdClass(OrderRecordId.class)
@Table(name = "order_record")
@Entity
public class OrderRecord {
@Id
@Column(name = "order_id")
private Integer orderId;
@Id
@Column(name = "product_id")
private Integer productId;
@ManyToOne
@JoinColumn(name = "customer_id", nullable = false)
private Customer customer;
private Integer quantity;
@Column(name = "order_date")
private LocalDateTime orderDate;
// 양방향 매핑: 배송 목록
@OneToMany(mappedBy = "orderRecord", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Delivery> deliveries = new ArrayList<>();
}
'JPA & QueryDSL' 카테고리의 다른 글
[QueryDSL] BooleanExpression을 이용한 WHERE 조건 표현식 (0) | 2025.03.07 |
---|---|
[JPA / Spring] JPQL을 이용해서 Entity가 아닌 DTO를 반환하는 방법 (0) | 2024.05.07 |
[JPA] 연관 관계 매핑 - @ManyToMany (0) | 2023.10.06 |
[JPA] 연관관계 매핑 (1) | 2023.10.06 |
[JPA] 연관관계 매핑 - @OneToOne 일대일 매핑 (0) | 2023.10.06 |