반응형

JPA 마스터 한줄 알았는데 나를 너무 과대평가했다.

이것 때문에 하루하고 반나절 해멨다....

 

내가 하려고 한 테이블 값을 가져오는 조건은 아래와 같다.

student 테이블의 일부컬럼만 가져와서 연산자(sum)와 group by 사용

매개변수로 가져온 날짜로 where절에 insert_date 필터 적용

student 테이블의 컬럼값은 student_id, student_name, address, phone, math_score, eng_score, kor_score 이다.

 

처음에 시도엔 일반적인 native query문처럼 사용하려고 하였다.

package com.beige0905.test.repository;

import com.beige0905.test.repository.vo.StudentVo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.List;

@Repository
public interface StudentTotalScore  extends JpaRepository<StudentVo, Long> {

    @Query(nativeQuery = true,
            value ="SELECT student_id AS student_id, student_name AS student_name," +
                    " SUM(math_score) AS math_score, SUM(eng_score) AS eng_score, SUM(kor_score) AS kor_score " +
                    " FROM student" +
                    " WHERE insert_date BETWEEN ?1 AND ?2" +
                    " GROUP BY student_id, student_name" +
                    " ORDER BY student_id")
    List<StudentVo> selectStudentScoreList(LocalDate startDate, LocalDate endDate);
}

이렇게 시도하는 경우 student 테이블의 컬럼값인 address, phone의 컬럼명을 찾을수 없습니다. 라는 오류를 보게 될 것이다.

 

해결하기 위해 SELECT절에 MAX(address), MAX(phone)를 추가하여 의미없는 값을 가져오면 해결은 되지만

이것보다 컬럼수가 더 많게 된다면 불필요한 값이 너무 많아지고, 코드도 복잡하게 된다.

 

찾아보다가 return 타입을 List<VO>가 아닌 List<Object[]>로 값을 받은 후 Service단에서 데이터를 가공하는 방법으로 변경하였다.

 

Repository

package com.beige0905.test.repository;

import com.beige0905.test.repository.vo.StudentVo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.List;

@Repository
public interface StudentTotalScore  extends JpaRepository<StudentVo, Long> {

    @Query(nativeQuery = true,
            value ="SELECT student_id AS student_id, student_name AS student_name," +
                    " SUM(math_score) AS math_score, SUM(eng_score) AS eng_score, SUM(kor_score) AS kor_score " +
                    " FROM student" +
                    " WHERE insert_date BETWEEN ?1 AND ?2" +
                    " GROUP BY student_id, student_name" +
                    " ORDER BY student_id")
    List<Object[]> selectStudentScoreList(LocalDate startDate, LocalDate endDate);
}

Service

public List<StudentVo> getStudentTotalScore(LocalDate startDate, LocalDate endDate) {
   List<Object[]> list = studentRepository.selectStudentScoreList(startDate, endDate);

   List<StudentVo> result = new ArrayList<>();
   for(Object[] obj : list) {
      result.add(StudentVo.builder()
            .studentId(obj[0].toString())
            .studentName(obj[1].toString())
            .mathScore(Long.parseLong(String.valueOf(obj[2])))
            .engScore(Long.parseLong(String.valueOf(obj[3])))
            .korScore(Integer.parseInt(String.valueOf(obj[4])))
            .build());
   }
   return result;
}

형변환은 math, eng는 long, kor는 int로 형변환 해봤다.

(Intege) (Long) 강제형변환은 적용이 안되어서 이렇게 형변환 함

 

https://stackoverflow.com/questions/22653858/how-to-get-multiple-columns-from-table-using-jpa

 

How to get multiple columns from table using JPA?

For example I have a table as Student it's contain columns like id, name, age I am reverting particular column values by using NativeQuery like below. Query query = entityManager.createNativeQuery("

stackoverflow.com

 

반응형
반응형

Pageable를 이용하여 Paging 처리를 하는 도중, Query문에서 SUM과 Group By를 이용하는 Native Query를 사용하게 되었다.

 

기존 방법대로 pageable을 이용한 JPA 코드는 굉장히 간단하다

만약 학생의 이름을 기준으로 학생 정보를 가져오고 싶은 경우 아래 코드와 같다.

package com.beige0905.test.repository;

import com.beige0905.test.repository.vo.StudentVo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<StudentVo, String> {
    Page<StudentVo> findByStudentName(String studentName, Pageable pageable);
}

 

하지만 집계함수와 Group By를 쓰고싶다면?

기본적으로 지원해주는 JPA 문법이 없기 때문에 Native Query를 사용해야 한다.

경로를 설정하여 xml 파일에 쿼리문을 모아두는 방법도 있지만 Query Annotation을 사용해서 작성하는게 개인적으로는 바로 쿼리문을 확인 할 수 있어서 좋은 것 같은데 뭐가 더 나은지는 아직 잘 모르겠다.

때문에 Query 어노테이션을 사용하여 아래와 같이 작성했다. 

Service

@Service
public class StudentService {

    @Autowired
    StudentRepository studentRepository;

    public Page<StudentVo> studentTotalScore(List<ColumnSearch> search, Pageable pageable) {
        log.info("search ==>" + search);
        //search ==>[StudentService.ColumnSearch(targetColumn=start, searchValue=2023-02-28), StudentService.ColumnSearch(targetColumn=end, searchValue=2023-03-06)]
        log.info("pageable ==>" + pageable);
        //pageable ==>Page request [number: 0, size 10, sort: studentId: ASC]

        String startDate = null;
        String endDate = null;

        for(ColumnSearch searchVo : search) {
            if(searchVo.getTargetColumn().equalsIgnoreCase("start")) {
                startDate = searchVo.getSearchValue();
            } else if(searchVo.getTargetColumn().equalsIgnoreCase("end")) {
                endDate = searchVo.getSearchValue();
            } else {
                log.info("check start date or end date");
            }
        }

        return studentRepository.getStudentTotalScore(startDate, endDate, pageable);
    }
}

Repository

package com.beige0905.test.repository;

import com.beige0905.test.repository.vo.StudentVo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

@Repository
public interface StudentRepository extends JpaRepository<StudentVo, String> {
@Query("SELECT v.studentId AS studentId, v.studentName AS studentName, SUM(v.mathScore) AS totalMathScore, "
        +"SUM(v.engScore) AS totalEngScore, v.className AS className, v.grade AS grade "
        +"FROM StudentVo v WHERE v.insertDate BETWEEN ?1 AND ?2 GROUP BY v.studentId, v.studentName, v.className, v.grade")
        Page<StudentVo> getStudentTotalScore(String startDate, String endDate, Pageable pageable);
        
}

VO

package com.beige0905.test.repository.vo;

import lombok.*;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "student_info")
public class StudentVo {
    @Id
    @Column(name = "student_id")
    private int studentId;

    @Column(name = "student_name")
    private String studentName;

    @Column(name = "math_score")
    private int totalMathScore;

    @Column(name = "eng_score")
    private int totalEngScore;

    @Column(name = "class_name")
    private String className;

    @Column(name = "grade")
    private int grade;

    @Column(name = "insert_date")
    private String insertDate;
}

 

이렇게 돌린다면?

 

아래와 같은 오류가 발생한다.

java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to {VO path}

로그를 찍어보니 Service단에 return type이 Page<StudentVo> 인데 repository의 결과를 로그로 찍어보면

[[value, value, value], [value, value, value]] 로 Object Array로 나와서 발생하는 오류이다.

구글링을 해보니 기존 JPA 쿼리는 Entity 객체를 결과로 만들지만 집계 함수가 있는 쿼리의 경우 Object 배열을 반환하기 때문에 내가 선언한 Page<VO>가 아닌 Page<Object>로 반환 된 것이다.

 

처음엔 JPA문으로 날짜를 필터한 후 List로 반환한 후, 반환한 List를 stream으로 Group By를 적용하는 방법을 시도하였는다.

하지만 pageable을 적용하는 부분에서 코드가 너무 복잡해져서 맞지 않다고 판단했다.

stream으로 적용했던 방법은 더보기에 있다. 왜 사용하지 않기로 판단했는지 알수 있다 ㅎㅎㅎ.... 

더보기
public Page<StudentVo> seachList(List<Search> search, Pageable pageable) {
		String startDate = null;
        String endDate = null;

        String[] orderBy = String.valueOf(pageable.getSort()).replaceAll(" ","").split(":");
        String aa = orderBy[0] + " " + orderBy[1];

        for(ColumnSearch searchVo : search) {
            if(searchVo.getTargetColumn().equalsIgnoreCase("start")) {
                startDate = searchVo.getSearchValue();
            } else if(searchVo.getTargetColumn().equalsIgnoreCase("end")) {
                endDate = searchVo.getSearchValue();
            } else {
                log.info("check start date or end date");
            }
        }
        
        List<StudentVo> result = studentRepository.getStudentTotalScore(startDate, endDate);

        Collections.sort(result);

        List<StudentVo> sortResult = new ArrayList<>();
        if(orderBy[1].equals("ASC")) {
            switch(orderBy[0]) {
                case "sudentId":
                    sortResult = result.stream().sorted(Comparator.comparing(StudentVo::sudentId)).collect(Collectors.toList());
                    break;
                case "studentName":
                    sortResult = result.stream().sorted(Comparator.comparing(StudentVo::studentName)).collect(Collectors.toList());
                    break;
                case "totalMathScore":
                    sortResult = result.stream().sorted(Comparator.comparing(StudentVo::totalMathScore)).collect(Collectors.toList());
                    break;
                case "totalEngScore":
                    sortResult = result.stream().sorted(Comparator.comparing(StudentVo::totalEngScore)).collect(Collectors.toList());
                    break;
                default:
                    log.info("ORDER BY ASC 컬럼명 오류");
                    break;
            }
        } else if(orderBy[1].equals("DESC")) {
            switch (orderBy[0]) {
                case "sudentId":
                    sortResult = result.stream().sorted(Comparator.comparing(StudentVo::sudentId).reversed()).collect(Collectors.toList());
                    break;
                case "studentName":
                    sortResult = result.stream().sorted(Comparator.comparing(StudentVo::studentName).reversed()).collect(Collectors.toList());
                    break;
                case "totalMathScore":
                    sortResult = result.stream().sorted(Comparator.comparing(StudentVo::totalMathScore).reversed()).collect(Collectors.toList());
                    break;
                case "totalEngScore":
                    sortResult = result.stream().sorted(Comparator.comparing(StudentVo::totalEngScore).reversed()).collect(Collectors.toList());
                    break;
                default:
                    log.info("ORDER BY DESC 컬럼명 오류");
                    break;
            }
        }

        PageRequest pageRequest = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize());
        int start = (int) pageRequest.getOffset();
        int end = Math.min((start + pageRequest.getPageSize()), sortResult.size());

        Page<StudentVo> page = new PageImpl<>(sortResult.subList(start, end), pageRequest, sortResult.size());
        log.info("Page : {} ", page.getContent());
    }
        return page;
}

 

열심히 삽질하다가 찾은 것이 Projection을 이용하는 방법이다.

 

먼저 필요한 기존VO에서 데이터들(DTO로 변환할 데이터)만 모은 인터페이스 VO를 생성한다.

Interface VO

package com.beige0905.test.repository.vo;

public interface IStudentVo {
    int getStudentId();
    String getStudentName();
    int getTotalMathScore();
    int getTotalEngScore();
}

기존 VO에 .map 함수를 이용하여 IStudentVo -> StudentVo로 Convert 해주는 method를 추가한다.

VO

package com.beige0905.test.repository.vo;

import lombok.*;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "student_info")
public class StudentVo {
	...
    
    public static StudentVo convertToPage(IStudentVo vo) {
        return StudentVo.builder()
                .studentId(vo.getStudentId())
                .studentName(vo.getStudentName())
                .totalMathScore(vo.getTotalMathScore())
                .totalEngScore(vo.getTotalEngScore())
                .build();
    }
}

Service단에 repository에서 return 받은 값을 map 함수에 convert method를 담아 변환한다.

Service

@Service
public class StudentService {

    @Autowired
    StudentRepository studentRepository;

    public Page<StudentVo> studentTotalScore(List<ColumnSearch> search, Pageable pageable) {
        log.info("search ==>" + search);
        //search ==>[StudentService.ColumnSearch(targetColumn=start, searchValue=2023-02-28), StudentService.ColumnSearch(targetColumn=end, searchValue=2023-03-06)]
        log.info("pageable ==>" + pageable);
        //pageable ==>Page request [number: 0, size 10, sort: studentId: ASC]

        String startDate = null;
        String endDate = null;

        for(ColumnSearch searchVo : search) {
            if(searchVo.getTargetColumn().equalsIgnoreCase("start")) {
                startDate = searchVo.getSearchValue();
            } else if(searchVo.getTargetColumn().equalsIgnoreCase("end")) {
                endDate = searchVo.getSearchValue();
            } else {
                log.info("check start date or end date");
            }
        }

        return studentRepository.getStudentTotalScore(startDate, endDate, pageable).map(StudentVo::convertToPage);
    }
}

 

 

참고자료

https://medium.com/@odysseymoon/spring-data-jpa%EC%97%90%EC%84%9C-groupby-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0-82cddc6e5d4a

 

Spring Data JPA에서 GroupBy 처리하기

Spring Data JPA에서는 GroupBy 를 사용하려면 어떻게 해야 할까요? Reference Doc 에서도 Group By 관련 키워드는 찾아보기 힘듭니다. Group By를 이용하려면 다른 방식을 적용해야 하는데요, Spring…

medium.com

https://beppy.hatenablog.com/entry/2022/06/11/162211

 

[Java]StreamのgroupingByで、カテゴリごとの合計(Sum)を求める方法 - 技術と日常。

前提 結果がMap int, long, doubleの場合 それ以外の型の場合 結果が元の型のCollection 参考 前提 例えば以下のような、カテゴリとその量が定義されているクラスがあったとします。 public record Obj( S

beppy.hatenablog.com

 

반응형
반응형

error log

Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: unexpected token: {VO class name} near line 1, column 249 [query문]

 

오류 코드

@Query("SELECT v.studentId AS studentId, v.studentName AS studentName, SUM(v.mathScore) AS mathScore, "
        +"SUM(v.engScore) AS engScore, v.className AS className, v.grade AS grade"
        +"FROM StudentVo v WHERE v.insertDate BETWEEN ?1 AND ?2 GROUP BY v.studentId, v.studentName, v.className, v.grade")

JPA를 사용한 Repository에서 나타난 오류이다

딱봐도 구문이 틀린 오류인데 쿼리문을 봤을때는 이상이 없어서 사실 249번까지 세었는데 로그에서도 틀린 부분이 확인되었다ㅎ

그렇다 FROM 앞에 띄어쓰기가 안되어서 그런거였다

 

수정코드

@Query("SELECT v.studentId AS studentId, v.studentName AS studentName, SUM(v.mathScore) AS mathScore, "
        +"SUM(v.engScore) AS engScore, v.className AS className, v.grade AS grade "
        +"FROM StudentVo v WHERE v.insertDate BETWEEN ?1 AND ?2 GROUP BY v.studentId, v.studentName, v.className, v.grade")
 

 

결론 : 로그를 잘보자

반응형

+ Recent posts