개발 사례
보통 entity class를 선언할 때 다음과 같이 @Id, @GeneratedValue를 사용하여 자동적으로 Id를 생성한다.
@Entity
@Table(name = "user")
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "nickname", nullable = false, unique = true, length = 15)
@NotBlank(message = "Nickname is required")
@Size(min = 2, max = 15)
@Pattern(regexp = "^[A-Za-z0-9가-힣]+$")
private String nickname;
}
하지만 만약 유저의 보유한 종목을 관리하는 UserStock entity가 있다고 해보자.
해당 entity에는 유저가 보유한 주식, 수익률, 보유 수량 등 정보가 있을 것이다.
그렇다면 설계적인 관점에서, 유저는 동일한 주식을 1개 이상 보유할 수 없다. 만약 동일한 주식을 추가 구매한다면
기존 정보를 update하는 방식으로 관리가 되어야 하는 것이다.
이러한 비즈니스 로직 관점에서 하나의 유저가 동일한 종목을 2개 이상 가질 수 없다는 비즈니스 규칙이 있다면,
유저와 주식의 ID인 user_id와 stock_id를 복합 키로 묶는것이 올바른 데이터 설계 방식일 것이다.
복합 키 클래스 생성 (@Embeddable)
이렇게 복합키를 정의하기 위해서는 @Embedabble 어노테이션을 사용한다.
@Embeddable
public class UserStockId implements Serializable {
@Column(name = "user_id")
private Long userId;
@Column(name = "stock_code")
private Long stockCode;
// 기본 생성자
public UserStockId() {}
public UserStockId(Long userId, Long stockCode) {
this.userId = userId;
this.stockCode = stockCode;
}
// equals()와 hashCode() 반드시 구현
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
UserStockId that = (UserStockId) o;
return Objects.equals(userId, that.userId) &&
Objects.equals(stockCode, that.stockCode);
}
@Override
public int hashCode() {
return Objects.hash(userId, stockCode);
}
}
이런식으로 Serializable 인터페이스를 구현하는 복합 키 객체를 정의한다.
이 때 클래스에 @Embeddable 어노테이션을 붙이고 필드에는 @Column를 정의해준다.
또한 이 복합 키 클래스는 equals와 hashCode를 항상 구현해야 한다.
만약 기본적인 비교만 수행한다면 @Data 어노테이션을 복합 키 클래스에 붙여주면 정의한 필드 값 기준으로 자동으로 생성한다.
Entity에 복합 키 추가 (@EmbeddedId)
@Entity
@Table(name = "user_stock")
@Setter
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserStock {
@EmbeddedId
UserStockId id;
@ManyToOne
@MapsId("userId")
@JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
@ManyToOne
@MapsId("stockCode")
@JoinColumn(name = "stock_code", referencedColumnName = "code")
private Stock stock;
@Column(name = "total_value", nullable = false)
private BigDecimal totalValue;
@Column(name = "price_per_unit", nullable = false)
private BigDecimal pricePerUnit;
@Column(name = "quantity", nullable = false)
private Integer quantity;
}
복합 키를 생성했으니 UserStock entity에 복합 키 필드를 선언한다.
이 때 @Id 대신 @EmbeddedId를 붙여준다.
또한 UserStock의 복합 키 필드는 해당 유저와 주식의 id를 가진다. 이렇게 복합 키 필드가 다른 entity를 참조하는데 쓰이는 경우 entity에서 해당 객체들을 참조하기 위해서는 @MapsId를 사용해주어야 한다.
복합 키 클래스인 UserStockId.class에는 user와 stock의 pk가 각각 userId와 sotckCode라는 변수명으로 정의되어 있다. 따라서 UserStock entity에서 User와 Stock entity를 참조하기 위해 각각 @MapsId("userId"), @MapsId("stockCode") 어노테이션을 붙여야 해당 entity 들이 참조 가능하다.
JpaRepository 정의
public interface UserStockRepository extends JpaRepository<UserStock, UserStockId> {
// 복합 키를 이용해 특정 유저와 주식의 관계를 조회
Optional<UserStock> findById(UserStockId id);
// 특정 유저가 보유한 모든 주식 조회
List<UserStock> findById_UserId(Long userId);
// 특정 주식을 보유한 모든 유저 조회
List<UserStock> findById_StockCode(Long stockCode);
}
복합 키를 사용하는 entity의 경우 JpaRepository의 두번째 타입을 복합 키 클래스로 지정한다.
또한 복합 키를 사용하는 CRUD 메소드를 선언할 때 전체를 키로 사용할 수 있고,
복합 키 내부의 특정 필드를 기준으로 조회할 수 있도록 메소드를 선언할 수 있다.