2022. 12. 23. 16:09ㆍProject/시네마그램
QLRM 라이브러리란
DB에서 Result 된 결과를 자바 클래스에 매핑해주는 역할을 합니다.
Repository에 nativeQuery를 직접 짜서 날리면 되는 거 아닌가?
Repository에 nativeQuery를 직접 짜서 넣으면 되는데 왜 굳이 QLRM 라이브러리를 추가해서 작업하는 것일까요?
그 이유는 특정 Repository는 특정 타입(Follow 모델)만 리턴하기 때문입니다. (상속받은 클래스 타입의 모델만 리턴)
따라서 직접 짠 쿼리의 결과가 특정 타입(Follow 모델)이 아니라면 Repository에 nativeQuery를 넣을 수가 없습니다.
- 실습할 코드는 DTO를 리턴해야 함
- 이처럼 DTO로 받아 내야 하는 쿼리의 경우 JpaRepository를 상속받은 인터페이스로 nativeQuery를 짜서 날릴 수 없다.
QLRM 라이브러리를 사용하는 이유
결론 :DTO에 DB 결과를 매핑하기 위함
JPA를 이용하면 영속성을 이용해서 Data 관리를 할 수 있습니다.
DB에 있는 데이터를 Entity로 받아주는데, 지금처럼 Entity가 아닌 API스펙에 맞는 DTO로 Data를 받고 싶을 때 사용할 수 있습니다.
build.gradle에 dependencies 추가
// QLRM
implementation group: 'ch.simas.qlrm', name: 'qlrm', version: '1.7.1'
QLRM 라이브러리가 제공해주는 JpaResultMapper객체를 만들면 Object 배열을 직접 매핑해줄 필요가 없는 이점이 있습니다.
mapper.list(①,②)
① 쿼리를 넣고
② 받고 싶은 DTO 클래스 타입을 넣어주면 자동으로 매핑해 줍니다.
하다 보니 총 세 번의 시도 끝에 성공하였습니다. 3단계로 나눠서 설명드리도록 하겠습니다.
필요한 클래스는 4개입니다.
- FollowInfoDto : 페이지유저가 팔로우 한 유저들을 출력하는 용으로 만든 DTO
- UserApiController
- FollowService : DTO로 Mapping 하기
- FollowRepositoryImpl
첫 번째 시도
FollowService에 쿼리를 짜서 DTO로 Mapping
FollowInfoDto
@NoArgsConstructor
@AllArgsConstructor
@Data
public class FollowInfoDto {
private Integer id;
private String username;
private String profileImageUrl;
private Integer followState;
private Integer equalUserState;
}
- Integer id → 타입을 int가 아닌 Integer로 쓴 이유
- int는 디버깅할 때 0이 나올 시 틀렸다고 출력
- 하지만, 경우에 따라 0이 정상적인 값일 수도 있습니다.
- 이런 경우 Interger를 쓰면 값이 없을 경우엔 null이 나와서 검증하기 편리합니다.
- id
- 로그인 한 유저가 팔로우, 언팔로우하려고 하는 상황에서
- '누가 누구를 팔로우하겠다.'에서 누구를 에 해당함
- toUserId
- equalUserState
- 구독 정보 모달에 출력된 user가 로그인한 유저와 동일인인가 확인하는 작업
- 아닌 경우에만 팔로우 버튼 노출
UserApiController
@GetMapping("/api/user/{pageUserId}/follow")
public ResponseEntity<?> followList(@PathVariable int pageUserId, @AuthenticationPrincipal CustomUserDetails customUserDetails) {
List<FollowInfoDto> followInfoDto = followService.followInfoList(customUserDetails.getUser().getId(), pageUserId);
return new ResponseEntity<>(new ResDto<>(1, "구독자 정보 리스트 불러오기 성공", followInfoDto), HttpStatus.OK);
}
FollowService
@RequiredArgsConstructor
@Service
public class FollowService {
private final FollowRepository followRepository;
@PersistenceContext
EntityManager em;
@Transactional(readOnly = true)
public List<FollowInfoDto> followInfoList(int sessionId, int pageUserId) {
// 1.
StringBuffer sb = new StringBuffer();
sb.append("SELECT u.id, u.username, u.profileImageUrl, ");
sb.append("if((SELECT 1 FROM follow WHERE fromUserId=? AND toUserId= u.id), 1, 0) followState, ");
sb.append("if((?=u.id), 1, 0) equalUserState ");
sb.append("FROM user u INNER JOIN follow f ");
sb.append("ON u.id = f.toUserId ");
sb.append("WHERE f.fromUserId=?");
// 2.
Query query = em.createNativeQuery(sb.toString())
.setParameter(1, sessionId)
.setParameter(2, sessionId)
.setParameter(3, pageUserId);
// 3.
JpaResultMapper result = new JpaResultMapper();
List<FollowInfoDto> list = result.list(query, FollowInfoDto.class);
return list;
}
- EntityManager em;
- 모든 Repository는 EntityManager를 구현해서 만든 구현체입니다.
- 1번
- 쿼리 준비 (동적으로 받는 부분? 처리)
- 이어지는 쿼리이므로 마지막에는 space처리
- 맨 끝 쿼리에 세미콜론 첨부 금지
- 2번
- 바인딩
- 3번
- QLRM 라이브러리를 사용해 쿼리 실행
- QLRM 라이브러리를 사용해 쿼리 실행
결과
생성자 없음 에러 발생
ERROR java.lang.RuntimeException: No constructor taking:
java.lang.Integer
java.lang.String
java.math.BigInteger
java.math.BigInteger
] with root cause
java.lang.RuntimeException: No constructor taking:
java.lang.Integer
java.lang.String
java.math.BigInteger
java.math.BigInteger
두 번째 시도
Service가 아닌 RepositoryImpl을 생성해서 쿼리 실행
UserApiController
@GetMapping("/api/user/{pageUserId}/follow")
public ResponseEntity<?> followList(@PathVariable int pageUserId, @AuthenticationPrincipal CustomUserDetails customUserDetails) {
List<FollowInfoDto> followInfoDto = followService.followList(customUserDetails.getUser().getId(), pageUserId);
return new ResponseEntity<>(new ResDto<>(1, "구독자 정보 리스트 불러오기 성공", followInfoDto), HttpStatus.OK);
}
FollowService
@Transactional(readOnly = true)
public List<FollowInfoDto> followList(int sessionId, int pageUserId) {
return followRepositoryImpl.followInfoList(sessionId,pageUserId);
}
FollowRepositoryImpl
@RequiredArgsConstructor
public class FollowRepositoryImpl {
private final EntityManager em;
public List<FollowInfoDto> followInfoList(int sessionId, int pageUserId) {
StringBuffer sb = new StringBuffer();
sb.append("SELECT u.id, u.username, u.profileImageUrl, ");
sb.append("if((SELECT 1 FROM follow WHERE fromUserId=? AND toUserId= u.id), 1, 0) followState, ");
sb.append("if((?=u.id), 1, 0) equalUserState ");
sb.append("FROM user u INNER JOIN follow f ");
sb.append("ON u.id = f.toUserId ");
sb.append("WHERE f.fromUserId=?");
Query query = em.createNativeQuery(sb.toString())
.setParameter(1, sessionId)
.setParameter(2, sessionId)
.setParameter(3, pageUserId);
JpaResultMapper result = new JpaResultMapper();
List<FollowInfoDto> list = result.list(query, FollowInfoDto.class);
return list;
}
}
결과
생성자 없음 에러 발생
ERROR java.lang.RuntimeException: No constructor taking:
java.lang.Integer
java.lang.String
java.math.BigInteger
java.math.BigInteger
] with root cause
java.lang.RuntimeException: No constructor taking:
java.lang.Integer
java.lang.String
java.math.BigInteger
java.math.BigInteger
두 방법 다 생성자가 없다는 ERROR가 발생했습니다. 따라서 원하는 대로 생성자를 만들어 주기로 했습니다.
세 번째 방법
생성자 만들기
FollowService
@RequiredArgsConstructor
@Service
public class FollowService {
private final FollowRepository followRepository;
@PersistenceContext
EntityManager em;
@Transactional(readOnly = true)
public List<FollowInfoDto> followInfoList(int sessionId, int pageUserId) {
StringBuffer sb = new StringBuffer();
sb.append("SELECT u.id, u.username, u.profileImageUrl, ");
sb.append("if((SELECT 1 FROM follow WHERE fromUserId=? AND toUserId= u.id), 1, 0) followState, ");
sb.append("if((?=u.id), 1, 0) equalUserState ");
sb.append("FROM user u INNER JOIN follow f ");
sb.append("ON u.id = f.toUserId ");
sb.append("WHERE f.fromUserId=?");
Query query = em.createNativeQuery(sb.toString())
.setParameter(1, sessionId)
.setParameter(2, sessionId)
.setParameter(3, pageUserId);
List<Object[]> results = query.getResultList();
List<FollowInfoDto> followInfoDtos = results.stream()
.map(o -> new FollowInfoDto(o))
.collect(Collectors.toList());
return followInfoDtos;
}
results리스트의 객체를 순회하며 FollowInfoDto생성자에 값으로 넣어주었습니다.
FollowInfoDto
@NoArgsConstructor
@Data
public class FollowInfoDto {
private Integer id;
private String username;
private String profileImageUrl;
private Integer followState;
private Integer equalUserState;
public FollowInfoDto(Object[] object) {
this.id = (int) object[0];
this.username = (String) object[1];
this.profileImageUrl = (String) object[2];
this.followState = Integer.parseInt(String.valueOf(object[3]));
this.equalUserState = Integer.parseInt(String.valueOf(object[4]));
}
}
DB
SELECT * FROM follow;
Postman 확인
생성자를 만들어주었더니 잘 출력됨을 확인할 수 있었습니다.
💡
nativeQuery의 결과가 엔티티 일경우 : Repository에서 nativeQuery 짜서 실행
nativeQuery의 결과가 DTO일 경우: QLRM 라이브러리사용해서 Service단에 쿼리 짜고 DTO Mapping 한 후 실행
참고:
https://jaewon2336.tistory.com/387
https://zlcjfalsvk.github.io/spring%20boot/springBoot-JpaDTO/
'Project > 시네마그램' 카테고리의 다른 글
[Trouble Shooting] 무한 순환참조 이슈 (0) | 2023.01.18 |
---|---|
[Cinemagram] 팔로우 기능 구현 - (9) (1) | 2022.12.26 |
[Cinemagram] Feed 페이지 렌더링 - (8) (0) | 2022.12.19 |
[Cinemagram] Image 업로드 및 렌더링 (feat. OSIV) - (7) (0) | 2022.12.14 |
[Cinemagram] JPA Native Query 팔로우 구현 및 예외처리 - (6) (0) | 2022.12.11 |