88일차 - JPA (커스텀 인터페이스, 정렬과 페이징 처리, GROUP BY, HAVING, Tuple 대신 Dto로 받기)

Yohan·2024년 6월 29일
0

코딩기록

목록 보기
130/156
post-custom-banner

Query Dsl

  • 주의사항
    -> 엔터티클래스에 필드추가되면 gradle 재 빌드하기
    build -> clean
    other -> complieQuerydsl

커스텀 인터페이스

  • 한 Repo에서 다중상속 받아서 여러가지 커스텀 쿼리 사용 가능
package com.spring.jpastudy.chap06_querydsl.repository;

import com.spring.jpastudy.chap06_querydsl.entity.Idol;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

// 여기서 다중상속을 받아 여러가지 커스텀 쿼리를 쓸 수 있음
public interface IdolRepository extends JpaRepository<Idol, Long>, IdolCustomRepository{
    @Query("SELECT i FROM Idol i ORDER BY i.age DESC")
    List<Idol> findAllBySorted();
}
  • 커스텀 쿼리들은 impl로 구현하여 사용
package com.spring.jpastudy.chap06_querydsl.repository;

import com.spring.jpastudy.chap06_querydsl.entity.Idol;

import java.util.List;

// 나만의 커스텀 인터페이스
public interface IdolCustomRepository {

    // 이름으로 오름차해서 전체조회
    List<Idol> findAllSortedByName();

    // 그룹명으로 아이돌을 조회
    List<Idol> findByGroupName();
}







package com.spring.jpastudy.chap06_querydsl.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import com.spring.jpastudy.chap06_querydsl.entity.Idol;
import com.spring.jpastudy.chap06_querydsl.entity.QIdol;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.spring.jpastudy.chap06_querydsl.entity.QIdol.*;

@Repository
@RequiredArgsConstructor
public class IdolRepositoryCustomImpl implements IdolCustomRepository {

    private final JdbcTemplate template;

    private final JPAQueryFactory factory;

    @Override
    public List<Idol> findAllSortedByName() {
        String sql = "SELECT * FROM tbl_idol ORDER BY idol_name ASC";
        return template.query(sql, (rs, n) -> {

            String idolName = rs.getString("idol_name");
            int age = rs.getInt("age");

            return new Idol(
                    idolName,
                    age,
                    null
            );
        });
    }

    // query dsl과 쿼리 연결
    @Override
    public List<Idol> findByGroupName() {
        return factory
                .select(idol)
                .from(idol)
                .orderBy(idol.group.groupName.asc())
                .fetch()
                ;
    }
}

  • service
    • Repo에서 다양한 방법으로 쿼리를 짤 수 있음
package com.spring.jpastudy.chap06_querydsl.service;

import com.spring.jpastudy.chap06_querydsl.entity.Idol;
import com.spring.jpastudy.chap06_querydsl.repository.IdolRepository;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional // JPA, QueryDsl 쓸 때 잊지말 것!
public class IdolService {

    // IdolRepository의 다중상속을 통해 JPQL, Mybatis, Query dsl 등 모두 쓸 수 있어
    // -> 하나만 써야되는거 아님
    private final IdolRepository idolRepository;

    // 아이돌을 나이 순으로 내림차 정렬해서 조회
    public List<Idol> getIdols() {
//        List<Idol> idolList = idolRepository.findAll();

        // 첫번째 방법
//        return idolList.stream()
//                .sorted(Comparator.comparing(Idol::getAge).reversed())
//                .collect(Collectors.toList());

        // 두번째 방법
//        List<Idol> idolList = idolRepository.findAllBySorted();
//        return idolList;

        // 세번째 방법 (커스텀 인터페이스)
        List<Idol> idolList = idolRepository.findByGroupName();
        return idolList;
    }
}

정렬과 페이징 처리

    @Test
    @DisplayName("QueryDSL로 기본 정렬하기")
    void sortingTest() {

        List<Idol> sortedIdols = factory
                .selectFrom(idol)
                .orderBy(idol.age.desc())
                .fetch();

        }
        
        
        
    @Test
    @DisplayName("페이징 처리 하기")
    void pagingTest() {
        //given
        int pageNo = 1;
        int amount = 2;

        //when
        List<Idol> pagedIdols = factory
                .selectFrom(idol)
                .orderBy(idol.age.desc())
                .offset((pageNo - 1) * amount) // limit 0,
                .limit(amount) //  2
                .fetch();

        // 총 데이터 수
        Long totalCount = factory
                .select(idol.count())
                .from(idol)
                .fetchOne();
                
		}
        
        
        
        
    @Test
    @DisplayName("나이 내림차순 정렬 및 페이징 처리 조회")
    void testSortByAgeDescAndPaging() {
        // given
        int pageNumber = 0; // 첫 번째 페이지
        int pageSize = 3; // 페이지당 데이터 수

        // when
        List<Idol> pagedIdols = factory
                .selectFrom(idol)
                .orderBy(idol.age.desc())
                .offset(pageNumber * pageSize)
                .limit(pageSize)
                .fetch();

        // then
        assertNotNull(pagedIdols);
        assertEquals(pageSize, pagedIdols.size());


        // 추가 검증 예시: 첫 번째 페이지의 첫 번째 아이돌이 나이가 가장 많은지 확인
        assertEquals("사쿠라", pagedIdols.get(0).getIdolName());
        assertEquals(26, pagedIdols.get(0).getAge());
    }

    @Test
    @DisplayName("특정 그룹의 아이돌을 이름 기준으로 오름차순 정렬 및 페이징 처리 조회")
    void testSortByNameAscAndPagingForGroup() {
        // given
        String groupName = "아이브";
        int pageNumber = 0; // 첫 번째 페이지
        int pageSize = 2; // 페이지당 데이터 수

        // when
        List<Idol> pagedIdols = factory
                .selectFrom(idol)
                .where(idol.group.groupName.eq(groupName))
                .orderBy(idol.idolName.asc())
                .offset(pageNumber * pageSize)
                .limit(pageSize)
                .fetch();

        // then
        assertNotNull(pagedIdols);
        assertEquals(pageSize, pagedIdols.size());

    }

QueryDsl의 GROUP BY, HAVING

    @Test
    @DisplayName("성별별, 그룹별로 그룹화하여 아이돌의 숫자가 3명 이하인 그룹만 조회")
    void groupByGenderTest() {
        //given
        List<Tuple> idolList = factory
                .select(idol.group, idol.gender, idol.count())
                .from(idol)
                .groupBy(idol.gender, idol.group)
                .having(idol.count().loe(3))
                .fetch();
        //when
        System.out.println("\n\n\n");

        for (Tuple tuple : idolList) {
            Group group = tuple.get(idol.group);
            String gender = tuple.get(idol.gender);
            Long count = tuple.get(idol.count());
            System.out.println(
                    String.format("\n그룹명 : %s, 성별: %s, 인원수: %d\n"
                            , group.getGroupName(), gender, count)
            );
        }

    }


    @Test
    @DisplayName("연령대별로 그룹화하여 아이돌 수를 조회")
    void ageGroupTest() {

        /*                   NATIVE SQL

            SELECT
                CASE age WHEN BETWEEN 10 AND 19 THEN 10
                CASE age WHEN BETWEEN 20 AND 29 THEN 20
                CASE age WHEN BETWEEN 30 AND 39 THEN 30
                END,
                COUNT(idol_id)
            FROM tbl_idol
            GROUP BY
                CASE age WHEN BETWEEN 10 AND 19 THEN 10
                CASE age WHEN BETWEEN 20 AND 29 THEN 20
                CASE age WHEN BETWEEN 30 AND 39 THEN 30
                END
         */

        //given
        // QueryDsl로 CASE WHEN THEN 표현식 만들기
        NumberExpression<Integer> ageGroupExpression = new CaseBuilder()
                .when(idol.age.between(10, 19)).then(10) // then은 조건에 만족하면 뭐라할건지.
                .when(idol.age.between(20, 29)).then(20)
                .when(idol.age.between(30, 39)).then(30)
                .otherwise(0); // .as() 쓰면 별칭 부여 가능

        //when
        List<Tuple> result = factory
                .select(ageGroupExpression, idol.count())
                .from(idol)
                .groupBy(ageGroupExpression)
                .fetch();
        }
    }


    @Test
    @DisplayName("그룹별로 그룹화해서 그룹 평균나이 조회, 평균나이 20~25 사이인 그룹만 조회")
    void groupByGroup() {

        /*           NATIVE SQL

            SELECT G.group_name, AVG(I.age)
            FROM tbl_idol I
            JOIN tbl_group G
            ON i.group_id = G.group_id
            GROUP BY G.group_id
            HAVING AVG(I.age) BETWEEN 20 AND 25
         */

        List<Tuple> result = factory
                .select(idol.group.groupName, idol.age.avg())
                .from(idol)
                .groupBy(idol.group)
                .having(idol.age.avg().between(20, 25))
                .fetch();
        //when
        assertFalse(result.isEmpty());

        }

    }
  • Tuple 말고 DTO로 받기!
    -> Projections객체를 사용
    -> dto에는 생성자가 꼭 있어야 함.
    -> Projections.constructor(내가 받을 타입=dto.class, 받을 것들)
  // Tuple 말고 DTO로 받기
    @Test
    @DisplayName("그룹별로 그룹화해서 그룹 평균나이 조회, 평균나이 20~25 사이인 그룹만 조회 (결과 DTO 처리)")
    void groupByGroupDto() {

        /*           NATIVE SQL

            SELECT G.group_name, AVG(I.age)
            FROM tbl_idol I
            JOIN tbl_group G
            ON i.group_id = G.group_id
            GROUP BY G.group_id
            HAVING AVG(I.age) BETWEEN 20 AND 25
         */

        // Projections : 커스텀 DTO를 포장해주는 객체
        // Projections.constructor(내가 받을 타입=dto.class, 받을 것 들)
        // DTO에 생성자가 있어야 함!
        List<GroupAverageAgeDto> result = factory
                .select(
                        Projections.constructor(
                                GroupAverageAgeDto.class,
                                idol.group.groupName,
                                idol.age.avg()
                        )
                )
                .from(idol)
                .groupBy(idol.group)
                .having(idol.age.avg().between(20, 25))
                .fetch();
        //when
        assertFalse(result.isEmpty());
        for (GroupAverageAgeDto dto : result) {
            String groupName = dto.getGroupName();
            double averageAge =dto.getAverageAge();

        }
    }
profile
백엔드 개발자
post-custom-banner

0개의 댓글