예약 시스템 개발하기 06. EmbeddedId와 ConverterNotFoundException
현재 예약 시스템은 화면에서 신청에 필요한 AccountId, 멤버십Id, 수업Id를 받아와 예약 히스토리에 해당 값을 insert하는 방식으로 진행한다. 이를 저장하기 위한 객체는 CourseReservation 객체였는데, 처음에는 AccountId만 PK로 지정했다가 수업Id와 AccountId를 같이 중복키로 등록해야 할 필요성이 생겼다.
이를 위해서 CourseReservationId라는 객체를 만들어주고 @Embeddable이라는 애노테이션을 붙여준다. 이렇게 PK만 따로 도메인을 만들어주면, 기존의 CourseReservation객체에서는 이를 주입받아서 사용하기만 하면 된다.
@Data
@Embeddable
@NoArgsConstructor
@AllArgsConstructor
public class CourseReservationId implements Serializable {
@Column(name="crs_id")
private Long crsId; // 수업아이디
@Column(name = "account_id", nullable = false)
private Integer accountId;
public CourseReservationId(Integer accountId, Long crsId) {
this.crsId = crsId;
this.accountId= accountId;
}
}
CourseReservation 객체에서는 중복키 객체를 등록하기 위해 @EmbeddedId 애노테이션을 붙여주면 된다.
@Data
@Entity
@Table(name = "crs_rsv")
public class CourseReservation {
@EmbeddedId
private CourseReservationId courseReservationId;
@Column(name = "mmbrshp_id", nullable = false)
private Integer mmbrshpId; // 멤버십 아이디
}
리포지토리 인터페이스도 아래와 같이 PK값에 Integer > CourseReservationId객체로 변환해주면 된다.
@Repository
public interface CourseReservationRepository extends JpaRepository<CourseReservation, CourseReservationId>
서비스 부분에서는 Repository의 매개변수를 생성할 때 CourseReservation과 CourseReservationId를 각각 셋팅하여 생성한다.
@Transactional
public CourseReservation saveCrsSrv(CourseReservationDto vo){
CourseReservation bean = new CourseReservation();
CourseReservationId id = new CourseReservationId(vo.getAccountId(), vo.getCrsId());
bean.setMmbrshpId(vo.getMmbrshpId());
bean.setCourseReservationId(id);
bean = courseReservationRepository.save(bean);
}
그리고 사용자가 예약한 수업리스트를 모두 가져오기 위해서 테스트 코드에 아래와 같은 로직을 추가해주었다.
List<CourseDTO> vo = courseReservationController.getReservedCourses(1);
System.out.println("reserved course: " + vo.toString());
테스트 결과는 ConverterNotFoundException 발생
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [com.core.euljiro.dto.CourseReservationDto]
CourseReservationDto는
@Data
@Builder
@NoArgsConstructor
public class CourseReservationDto {
@NotNull
private Long crsId; // 수업아이디
@NotNull
private Integer mmbrshpId; // 멤버십 아이디
@NotNull
private Integer accountId;
@Builder
public CourseReservationDto(Long crsId, Integer mmbrshpId , Integer accountId){
this.crsId = crsId;
this.mmbrshpId = mmbrshpId;
this.accountId = accountId;
}
}
Repository에서 JPQL이 이렇게 작성되어 있어 문제가 발생하였다.
@Repository
public interface CourseReservationRepository extends JpaRepository<CourseReservation, CourseReservationId> {
@Query(value="select p.crs_id as crsId, p.mmbrshp_id as mmbrshpId , p.account_id as accountId from crs_rsv p " +
"where p.account_id = :userId", nativeQuery = true)
public List<CourseReservationDto> findByUserId(@Param("userId") int userId);
}
native 쿼리 사용시 도메인으로 리턴을 받지 않고 Dto로 받아야 할 때 Converter를 작성해주어야 하는데 명시된 Converter가 없어서 나는 오류다(https://www.popit.kr/jpa-native-query-%EC%82%AC%EC%9A%A9-%EC%8B%9C-dto%EB%A1%9C-%EB%A7%A4%ED%95%91%ED%95%98%EA%B8%B0/)
그렇다... 리턴 타입에 Dto가 들어가있었다. 나는 도메인으로 리턴을 받을 생각이었는데....
그런데 알아보던 중, 중복키를 주입받았을 때 중복키 객체를 기준으로 쿼리 메소드를 작성하는 방법이 스프링 JPA 문서에 나와있었다.
https://www.baeldung.com/spring-jpa-embedded-method-parameters
// 1. Book 도메인
@Entity
public class Book {
@EmbeddedId
private BookId id; // id라는 이름으로 중복키 객체 생성
private String genre;
private Integer price;
//standard getters and setters
}
// 2. findById + 중복키 객체 내 키 name 으로 쿼리 메소드 생성
@Repository
public interface BookRepository extends JpaRepository<Book, BookId> {
List<Book> findByIdName(String name);
List<Book> findByIdAuthor(String author);
}
// 3. Jpa에서 자동으로 conversion 해줌
findByIdName -> directive "findBy" field "id.name"
findByIdAuthor -> directive "findBy" field "id.author"
나는 중복키 객체를
@EmbeddedId
private CourseReservationId courseReservationId;
이렇게 선언했으므로 쿼리메소드는 courseReservationIdAccountId로 작성하면 directive "findBy" field courseReservation.AccoutId 로 변환해주겠지! 아래와 같이 리포지토리 소스를 변경해주었다.
@Repository
public interface CourseReservationRepository extends JpaRepository<CourseReservation, CourseReservationId> {
// @Query(value="SELECT crs_id as crsId, mmbrshp_id as mmbrshpId , account_id as accountId from crs_rsv " +
// "where account_id = :userId", nativeQuery = true)
public List<CourseReservation> findByCourseReservationIdAccountId(@Param("userId") int userId);
}
하.. 뿌듯한 마음으로 회먹으러가야징