프로젝트 진행 중 아래 그림의 '리뷰 좋아요' 테이블을 위해 외래키들로 이뤄진 복합키를 사용해야하는 상황이 생겼다.
review_id는 리뷰 테이블에서 참조하여 받아오는 외래키이고, 마찬가지로 user_id도 사용자 테이블에서 받아오는 외래키이다.
이전에 Spring JPA에서 복합키를 사용해본 적이 없어서 진행하는데 큰 어려움이 있었다.
다음에 또 이런 상황이 발생한다면 시행착오를 줄이고 어려움 없이 진행하기 위해 제대로 정리해보고자 한다.
또한 나처럼 외래키와 복합키를 동시에 사용해야 하는 다른 개발자들에게도 이 글이 도움이 되었으면 좋겠다.
시작하기에 앞서 외래키를 받아올 부모 테이블인 Review 테이블의 코드이다.
@Getter
@Setter
@NoArgsConstructor
@Entity
public class Review extends BaseTimeEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long reviewId;
// 사용자가 아직 구현되지 않아 외래키로 삼지 않음
@Column(nullable = false)
private Long userId;
...
}
이제 Review의 PK인 reviewId를 FK로 받아올 ReviewLike 테이블을 만들면 된다. 다만 위의 ERD 그림에서 확인할 수 있듯이 복합키로 받아와야 한다.
간단하게 생각하면...?
간단하게 생각해서 복합키를 사용하기 위해서 아래 코드와 같이 ID로 삼으려는 Field위에 @Id 어노테이션을 붙이면 되는 것이 아닌가 생각할 수 있다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class ReviewLike extends BaseTimeEntity implements Serializable {
@Id
@ManyToOne(targetEntity = Review.class, fetch = FetchType.LAZY) // 리뷰 테이블을 참조하는
@JoinColumn(name = "reviewId")
private Review review; // 외래키
@Id // 사용자 테이블의 구현이 완료되지 않아 userId는 현재 외래키로 삼지 않음
private Long userId;
}
하지만 위와 같이 @Id를 복합키로 삼을 필드 위에 붙이기만 하면 아래와 같은 오류가 발생한다.
This class [class cotato.Bookluetooth.review.like.ReviewLike] does not define an IdClass
즉, ReviewLike 테이블을 위해 IdClass를 정의해줘야 한다는 것이다.
※ 참고로 JPA에서 복합키를 사용하기 위해선 다음의 조건들을 만족 해야한다.
- The composite primary key class must be public.
- It must have a no-arg constructor.
- It must define the equals() and hashCode() methods.
- It must be Serializable.
IdClass 적용
IdClass는 복합키를 가지는 테이블 엔티티에 @IdClass(엔티티Id.class)와 같은 어노테이션을 붙임으로써 적용할 수 있다.
먼저 ReviewLike를 위한 IdClass를 생성한다.
@Setter
@Getter
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
public class ReviewLikeId implements Serializable {
private Long review; // 외래키의 실제 타입(Long)을 명시하고, 해당 테이블에서의 필드명(review)과 일치시켜야함
private Long userId;
}
여기서 중요한 점은 주석에서 달아놓았듯이 외래키의 실제 타입을 명시하여야 한다.
해당 테이블(ReviewLike)에서의 review 필드의 타입인 Review가 아닌 실제 타입인 Long을 명시하여야 한다.
그렇지 않으면 실제로 데이터들을 처리할 때, 타입이 맞지 않는다고 오류가 발생한다.
신기하게도 서비스단에서 Review 객체를 넘겨주지만 자동으로 해당 객체에서 Long 타입의 PK의 값을 가져오는 것 같다.
이 때문에 1~2시간 이상 같은 오류로 삽질을 반복했다....
마지막으로 복합키를 사용할 테이블인 ReviewLike 클래스에 다음과 같이 @IdClass 어노테이션을 추가해주면 복합키 사용 설정이 완료된다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@IdClass(ReviewLikeId.class) // 추가
public class ReviewLike extends BaseTimeEntity implements Serializable {
@Id
@ManyToOne(targetEntity = Review.class, fetch = FetchType.LAZY)
@JoinColumn(name = "reviewId")
private Review review;
@Id
private Long userId;
}
@EmbeddedId??
위에서 사용한 @IdClass 외에도 @EmbeddedId를 사용하여 복합키를 사용할 수 있다고 한다.
@Embeddable // 차이점
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
public class ReviewLikeId implements Serializable {
@Column(name = "reviewId") // 차이점
private Long review;
private Long userId;
}
@IdClass와의 차이점은 @Embeddable를 클래스에 붙여줘야한다는 점과 @Column 어노테이션을 사용하고자 한다면 정의해줘야 한다는 점이다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class ReviewLike extends BaseTimeEntity implements Serializable {
@EmbeddedId
private ReviewLikeId reviewLikeId; // 차이점
@MapsId("review") // 차이점
@ManyToOne(targetEntity = Review.class, fetch = FetchType.LAZY)
@JoinColumn(name = "reviewId")
private Review review;
@MapsId("userId") // 차이점
private Long userId;
}
@IdClass와의 차이점은 @Id 대신 @MapsId를 사용했다는 점과 @EmbeddedId를 사용했다는 점이다.
@MapsId에는 reviewLikeId의 필드들을 각각 맞게 주입해주면 된다.
@IdClass vs @EmbeddedId
@IdClass는 같은 형태의 칼럼을 복합키 클래스와 엔티티 클래스에 두 번 정의해 줘야한다.
반면 @EmbeddedId를 사용하는 경우에는 복합키 클래스에만 칼럼을 정의해주면 된다.
값을 조회할 때도 차이가 난다.
@IdClass를 사용하는 경우 아래와 같이 PK의 값을 확인할 수 있다.
member.getTeamId()
@EmbeddedId를 사용하는 경우 한 단계 더 들어가야 PK의 값을 조회할 수 있다.
member.getMemberId().getTeamId()
그리고 @IdClass의 경우 복합키 클래스를 활용하여 객체지향적인 프로그래밍이 불가능하다는 특징이 있어 복합키 칼럼에 대한 수정이 이루어지지 않는 엔티티에서 활용하기 좋다는 특징이 있다.
반면 @EmbeddedId는 복합키 클래스를 활용하여 객체지향적인 프로그래밍을 통해 복합키 칼럼들을 다룰 수 있다는 특징이 있다.
복합키를 사용할 경우 Repository는?
public interface ReviewLikeRepository extends JpaRepository <ReviewLike,ReviewLikeId> {
}
위의 @IdClass나 @EmbeddedId를 사용하여 복합키 설정을 마쳤다면, 위의 코드처럼 ReviewLike 테이블과 ID 제네릭 타입인 복합키 클래스(ReviewLikeId)를 주입함으로써 Repository를 정의할 수 있다.
복합키를 사용한 경우 POST, GET, DELETE 구현
// POST
@Transactional
public void saveLike(ReviewLikeRequestDto requestDto) {
Long targetId = requestDto.getReviewId();
Review review = reviewRepository.findById(targetId)
.orElseThrow(() -> new IllegalArgumentException("해당 리뷰가 존재하지 않습니다. reviewId = " + targetId));
ReviewLike newReviewLike = new ReviewLike(review, requestDto.getUserId());
reviewLikeRepository.save(newReviewLike);
}
// GET
@Transactional
public List<ReviewLikeResponseDto> findLike(Long reviewId) {
return reviewLikeRepository.findByReview_ReviewId(reviewId).stream()
.map(ReviewLikeResponseDto::new)
.collect(Collectors.toList());
}
// DELETE
@Transactional
public void dislike(ReviewLikeRequestDto requestDto) {
ReviewLike like = reviewLikeRepository.findById(new ReviewLikeId(requestDto.getReviewId(),requestDto.getUserId()))
.orElseThrow(() ->
new IllegalArgumentException("해당 리뷰가 존재하지 않습니다. reviewId = " + requestDto.getReviewId()));
reviewLikeRepository.delete(like);
}
서비스단에서 POST, GET, DELETE를 구현한 코드이다.
참조
'개발 > 스프링 개념' 카테고리의 다른 글
[Spring] DAO vs DTO vs VO vs Entity (0) | 2023.08.16 |
---|---|
[Spring] Spring JPA FindByFK (외래키로 조회) (0) | 2023.02.15 |
[Spring] Session이란? (0) | 2023.01.25 |
[Spring] Select에 @Transaction을 사용하는 이유 (0) | 2023.01.20 |
[Spring] Spring 웹 계층 구조 (0) | 2023.01.20 |