[Trouble Shooting] JPA DTO Mapping - QLRM 라이브러리 사용

2022. 12. 23. 16:09Project/시네마그램

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 라이브러리를 사용해 쿼리 실행
           

 

결과

생성자 없음 에러 발생

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 한 후 실행

 

 

 

 

 

 

 

참고:

Dependency에 추가할 라이브러리 찾는 곳

 

https://jaewon2336.tistory.com/387

https://zlcjfalsvk.github.io/spring%20boot/springBoot-JpaDTO/