[JPA] 11.Querydsl

재우·2025년 12월 27일

JPA

목록 보기
11/11

기타

  • JPQL을 자바코드로 작성하기 위해서는 Q타입(=Q타입 클래스)를 생성해야한다.
    Q타입은 엔티티를 기반으로 생성된 쿼리 전용 클래스이다. Q타입은 쿼리를 자바코드로 작성할때 필요하다. 쿼리를 자바코드로 작성할때에는 Q타입 안에 미리 만들어져 있는 Q타입 객체를 사용한다.
    즉, 엔티티를 기반으로 Q타입 클래스가 빌드 시점에 생성되고, 쿼리 작성 시에는 Q타입 클래스에 미리 정의된 Q타입 객체를 사용한다.

  • 참고로 Q타입은 Gradle 빌드 과정 중 자바 컴파일 단계에서 생성된다.
    그래서 터미널에서 ./gradlew build나 ./gradlew compileJava나 ./gradlew compileQuerydsl을 실행하면 Q타입이 생성된다.



JPQL vs Querydsl

  • Querydsl은 라이브러리이다.

  • Querydsl은 JPQL 빌더이다 = Querydsl은 JPQL을 문자열로 직접 작성하지 않고, 자바 코드로 JPQL을 조립(빌드)해주는것이다.
    타입세이프하다. => JPQL 문자열에서 런타임에 터질 에러를 컴파일 시점에 잡아준다.

  • Querydsl에서 JPQL을 실행하려면 JPAQueryFactory가 있어야하고, EntityManager을 통해 JPAQueryFactory를 생성한다.
    그리고 Querydsl은 fetchOne(), fetch(), 같은 fetch 계열 메서드가 호출되는 순간 JPQL을 실행한다.



기본 Q-Type 활용

import study.querydsl.entity.Team; // import
import static study.querydsl.entity.QMember.*; // static import

  • static import를 하게 되면, 클래스 이름 없이 static 필드/메서드를 바로 사용 가능하다.
    즉, QMember.member는 QMember클래스에 있는 static필드에 접근한 것인데,
    이를 static import하면 바로 member로 사용할수 있다.

static import 사용전에는

Member findMember = queryFactory
                .select(QMember.member)
                .from(QMember.member)
                .where(QMember.member.username.eq("member1"))
                .fetchOne();

static import 사용후에는

import static study.querydsl.entity.QMember.*;

Member findMember = queryFactory
                .select(member)
                .from(member)
                .where(member.username.eq("member1"))
                .fetchOne();


페이징

	@BeforeEach
    public void before() { // 각 @Test 실행전에 실행됨.
        queryFactory = new JPAQueryFactory(em);
        Team teamA = new Team("teamA");
        Team teamB = new Team("teamB");

        em.persist(teamA);
        em.persist(teamB);

        Member member1 = new Member("member1", 10, teamA);
        Member member2 = new Member("member2", 20, teamA);
        Member member3 = new Member("member3", 30, teamB);
        Member member4 = new Member("member4", 40, teamB);

        em.persist(member1);
        em.persist(member2);
        em.persist(member3);
        em.persist(member4);
    }

위와 같이 있다고 했을때,

    @Test
    public void test() {
        QueryResults<Member> results = queryFactory.selectFrom(member)
                .offset(1)
                .limit(2)
                .fetchResults();
        System.out.println("!!!!" + results.getTotal()); // 4 (전체 데이터 개수)
        System.out.println("@@@@" + results.getLimit()); // 2
        System.out.println("####" + results.getOffset()); // 1
        List<Member> resultList = results.getResults();
        for (Member member1 : resultList) {
            System.out.println("member = " + member1); 
        }
    } // 2번째(offset)부터 2개(limit)를 조회하는 쿼리


	@Test
    public void test1() {
        Pageable pageable = PageRequest.of(0, 2); // (여기서 첫번째 파라미터는 페이지번호 두번째 파라미터는 페이지 별 사이즈)
        QueryResults<Member> results = queryFactory.selectFrom(member)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();
        System.out.println("!!!!" + results.getTotal()); // 4 (전체 데이터 개수)
        System.out.println("@@@@" + results.getLimit()); // 2 ( 페이지 별 사이즈)
        System.out.println("####" + results.getOffset()); // 0 ( pageable.getOffset() 값은 페이지번호 x 페이지 별 사이즈이다 )
        List<Member> resultList = results.getResults();
        for (Member member1 : resultList) {
            System.out.println("member = " + member1);
        }
    } // 첫번째페이지에 2개의 데이터를 조회하는 페이징 쿼리 ( offset이 0이므로, 첫번째부터 2개의 데이터를 조회하는 페이징 쿼리)

	@Test
    public void test2() {
        Pageable pageable = PageRequest.of(1, 2); // (여기서 첫번째 파라미터는 페이지번호 두번째 파라미터는 페이지 별 사이즈)
        QueryResults<Member> results = queryFactory.selectFrom(member)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetchResults();
        System.out.println("!!!!" + results.getTotal()); // 4 (전체 데이터 개수)
        System.out.println("@@@@" + results.getLimit()); // 2 ( 페이지 별 사이즈)
        System.out.println("####" + results.getOffset()); // 2 ( pageable.getOffset() 값은 페이지번호 x 페이지 별 사이즈이다 )
        List<Member> resultList = results.getResults();
        for (Member member1 : resultList) {
            System.out.println("member = " + member1);
        }
    } // 두번째페이지에 2개의 데이터를 조회하는 페이징 쿼리 (offset이 2이므로, 세번째부터 2개의 데이터를 조회하는 페이징 쿼리)


조인

inner join이든 left join이든

  • 연관관계가 있는 엔티티와 조인을 하면, on을 쓰지않더라도 SQL로 변환될때 on member0.team_id=team1.id 와 같은 쿼리가 생긴다.
  • 연관관계가 없는 엔티티와 조인을 하면, on을 무조건 써야하고 SQL로 변환될때 on에서 작성한 내용만 쿼리로 생긴다.
  • 연관관계가 있으면 조인할때 파라미터를 2개 주어야하고, 없으면 1개만 주면된다.
    • member엔티티와 연관된 team엔티티와 조인하기 위해서는 join(member.team, team)처럼 첫번째 파라미터에는 member와 연관된 team엔티티를 적고, 두번째 파라미터에는 member와 연관된 team엔티티의 별칭을 지정해주어야한다.
    • member엔티티와 연관관계가 없는 team엔티티와 조인하기 위해서는 join(team) 처럼 team엔티티만 적어주면되고, 이게 곧 엔티티를 지정함과 동시에 별칭이 되는것이다.
    • 그래서 이 별칭을 on절이나 where절 등에서 사용한다.

연관관계가 있을떄 join

List<Member> result = queryFactory
                .selectFrom(member)
                .join(member.team, team)
                .on(team.name.eq("teamA"))
                .fetch();
select
	member1 
from
	Member member1   
inner join
        member1.team as team 
with 
	team.name = ?1

와 같은 jpql이 실행되고, 

select
	member0_.member_id as member_i1_1_,
	member0_.age as age2_1_,
	member0_.team_id as team_id4_1_,
	member0_.username as username3_1_ 
from
	member member0_ 
inner join
	team team1_ 
on member0_.team_id=team1_.id 
	and (
		team1_.name=?
	)

와 같이 SQL로 변환되서 실행된다.

연관관계가 있을떄 left join

List<Member> result = queryFactory
                .selectFrom(member)
                .leftJoin(member.team, team)
                .on(team.name.eq("teamA"))
                .fetch();
select
	member1 
from
	Member member1   
left join
	member1.team as team 
with 
	team.name = ?1

와 같은 jpql이 실행되고, 

select
	member0_.member_id as member_i1_1_,
	member0_.age as age2_1_,
	member0_.team_id as team_id4_1_,
	member0_.username as username3_1_ 
from
	member member0_ 
left outer join
	team team1_ 
on member0_.team_id=team1_.id 
	and (
		team1_.name=?
	)
와 같이 SQL로 변환되서 실행된다.

연관관계가 없을떄 join

List<Member> result = queryFactory
                .selectFrom(member)
                .join(team)
                .on(team.name.eq("teamA"))
                .fetch();
select
	member1 
from
	Member member1   
inner join
	Team team 
with 
	team.name = ?1

와 같은 jpql이 실행되고, 

select
	member0_.member_id as member_i1_1_,
	member0_.age as age2_1_,
	member0_.team_id as team_id4_1_,
	member0_.username as username3_1_ 
from
	member member0_ 
inner join
	team team1_ 
on (
	team1_.name=?
	)
와 같이 SQL로 변환되서 실행된다.

연관관계가 없을떄 left join

List<Member> result = queryFactory
                .selectFrom(member)
                .leftJoin(team)
                .on(team.name.eq("teamA"))
                .fetch();
select
	member1 
from
	Member member1   
left join
	Team team 
with 
	team.name = ?1

와 같은 jpql이 실행되고, 

select
	member0_.member_id as member_i1_1_,
	member0_.age as age2_1_,
	member0_.team_id as team_id4_1_,
	member0_.username as username3_1_ 
from
	member member0_ 
left outer join
	team team1_ 
on (
	team1_.name=?
	)
와 같이 SQL로 변환되서 실행된다.


페치조인

  • 연관된 엔티티나 컬렉션을 SQL한번에 조회

참고로,

  • EntityManagerFactory는 애플리케이션이 실행될때 애플리케이션 전역에서 단 하나만 생성되며, 실제 EntityManager를 생성하는 객체이고,
  • EntityManager 프록시(싱글톤)는 애플리케이션이 실행될때 애플리케이션 전역에서 단 하나만 생성되며, 이때 생성되는 EntityManger 프록시는 실제 EntityManger가 아니다.
    그래서 주입되는 스프링빈도 프록시객체이다.(@Autowired EntityManager em)
    실제 EntityManger는 트랜잭션이 시작할때 트랜잭션 단위로 생성되며, 프록시가 실제 EntityManger에게 작업을 위임한다. 실제 EntityManger가 영속성 작업을 수행하는 객체이다.
    즉, em.persist()를하면 프록시가 persist()하는것이지만 내부적으로 실제 EntityManager가 생성되서 이 실제 EntityManger에게 persist()를 위임해서 이 실제 EntityManger가 persist()를 하는것이다.
  • 트랜잭션 단위로 실제 EntityManager가 생성될때 이때 EntityManagerFactory에 의해 생성된다.
  • @PersistenceContext에 의해 EntityManager를 자동으로 의존성주입 할 수 있고, @PersistenceUnit에 의해 EntityManagerFactory를 자동으로 의존성주입 할 수 있다.
    그리고 둘다 @AutoWired로도 의존성 주입 할 수 있다.
	@PersistenceUnit
	EntityManagerFactory emf;

	@Test
    public void fetchJoin() {
        em.flush();
        em.clear();

		Member findMember = queryFactory
                .selectFrom(member)
                .join(member.team)
                .fetchJoin()
                .where(member.username.eq("member1"))
                .fetchOne();

        boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam()); // 해당 객체가 초기화 되었는지 확인
        assertThat(loaded).isTrue();
 	}
  • 참고로 연관관계가 있는것에 대해서만 페치조인 할 수 있다.
    즉, join(team).fetchJoin()처럼 하면 안된다.
select
	member0_.member_id as member_i1_1_0_,
	team1_.id as id1_2_1_,
	member0_.age as age2_1_0_,
	member0_.team_id as team_id4_1_0_,
	member0_.username as username3_1_0_,
	team1_.name as name2_2_1_ 
from
	member member0_ 
inner join
	team team1_ 
on 
	member0_.team_id=team1_.id 
where
	member0_.username=?
  • JPQL로 직접 join fetch하는것과 동일한 SQL로 변환되서 실행된다.


서브쿼리

JPAExpressions를 활용해서 서브쿼리를 작성한다.

일반적인 SQL에서 서브쿼리 작성하는것 처럼, 쿼리에서 사용하고 있는 테이블의 alias와 겹치면 안되므로, 별도의 QMember를 만들어서 서브쿼리에서 활용한다.

  • case1
public void subQuery() {
        QMember memberSub = new QMember("memberSub");

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.eq(
                        JPAExpressions
                                .select(memberSub.age.max())
                                .from(memberSub)
                ))
                .fetch();
        assertThat(result).extracting("age")
                .containsExactly(40);
    }
[JPQL]
select
        member1 
    from
        Member member1 
    where
        member1.age = (
            select
                max(memberSub.age) 
            from
                Member memberSub
        )
[SQL]
select
            member0_.member_id as member_i1_1_,
            member0_.age as age2_1_,
            member0_.team_id as team_id4_1_,
            member0_.username as username3_1_ 
        from
            member member0_ 
        where
            member0_.age=(
                select
                    max(member1_.age) 
                from
                    member member1_
            )
  • case2
public void subQueryIn() {
        QMember memberSub = new QMember("memberSub");

        List<Member> result = queryFactory
                .selectFrom(member)
                .where(member.age.in(
                        JPAExpressions
                                .select(memberSub.age)
                                .from(memberSub)
                                .where(memberSub.age.gt(10))
                ))
                .fetch();
        assertThat(result).extracting("age")
                .containsExactly(20, 30, 40);
    }
[JPQL]
select
        member1 
    from
        Member member1 
    where
        member1.age in (
            select
                memberSub.age 
            from
                Member memberSub 
            where
                memberSub.age > ?1
        )
[SQL]
select
            member0_.member_id as member_i1_1_,
            member0_.age as age2_1_,
            member0_.team_id as team_id4_1_,
            member0_.username as username3_1_ 
        from
            member member0_ 
        where
            member0_.age in (
                select
                    member1_.age 
                from
                    member member1_ 
                where
                    member1_.age>?
            )


CASE문

	@Test
    public void basicCase() {
        List<String> result = queryFactory
                .select(member.age
                        .when(10).then("열살")
                        .when(20).then("스무살")
                        .otherwise("기타"))
                .from(member)
                .fetch();

        for (String s : result) {
            System.out.println("s = " + s);
        }
    }
	@Test
    public void complexCase() {
        List<String> result = queryFactory
                .select(new CaseBuilder()
                        .when(member.age.between(10, 20)).then("0~20살")
                        .when(member.age.between(21, 30)).then("21살~30살")
                        .otherwise("기타"))
                .from(member)
                .fetch();

        for (String s : result) {
            System.out.println("s = " + s);
        }
    }


DTO 조회

  1. 프로퍼티 접근
    .select(Projections.bean(MemberDto.class,
        member.userName,
        member.age
        )
    ))
    .from(member)
  • 기본 생성자를 통해 MemberDto를 만들고 해당 필드에 값을 setter하는 방식
    MembeDto dto = new MemberDto(); // 기본 생성자 호출
    dto.setUsername(resultUsername); // setter 호출
    dto.setAge(resultAge); // setter 호출
  1. 필드 접근
    .select(Projections.fields(MemberDto.class,
        member.userName,
        member.age
        )
    ))
    .from(member)
  • 기본 생성자를 통해 MemberDto를 만들고 해당 필드에 값을 저장하는 방식
    MemberDto dto = new MemberDto(); // 기본 생성자 호출
    dto.username = resultUsername; // 직접 필드에 저장
    dto.age = resultAge; // 직접 필드에 저장
  1. 생성자 사용
    .select(Projections.constructor(MemberDto.class,
        member.userName,
        member.age
        )
    ))
    .from(member)
  • JPQL에서 new Dto(...) 하는 것과 동일한 생성자 주입 방식이다.

조회하려는 member의 "userName"과 MemberDto의 "userName"필드명은 동일해야한다. 다르다면 값이 들어오지않고, alias를 사용해서 동일하게 해야한다.

1.
.select(Projections.fields(MemberDto.class,
    member.userName.as("memberName"),
    member.age.as("memberAge")
    )
))
.from(member)

2.
.select(Projections.fields(MemberDto.class,
    member.userName.as("memberName"),     
    ExpressionUtils.as(                  
        member.age.add(1),
        "memberAge"
    )
))
.from(member)

3.
List<MemberDto> result = queryFactory
    .select(Projections.fields(MemberDto.class,
        member.userName.as("memberName"),
        ExpressionUtils.as(
            JPAExpressions
                .select(subMember.age.max())
                .from(subMember),
            "memberAge"
        )
    ))
    .from(member)
    .fetch();
  • member.userName처럼 필드에 바로 접근했을때만 바로 뒤에 .as로 alias를 줄 수 있고,
  • member.age.add(1)나 JPAExpressions.select(...) 등 이외 방식에서는 ExpressionUtils.as를 사용해야한다.
  • ExpressionUtils.as(expr, "alias") => SQL로 보면 expr AS alias와 같다.
  • as와 ExpressionUtils.as는 Projections.fields든 Projections.bean이든 Projections.constructor든 상관없이 사용할 수 있다.

참고로,
DTO 클래스의 생성자에 @QueryProjection을 달아주고 빌드나 ./gradlew compileQuerydsl을 하면 Q파일이 생긴다.

List<MemberDto> result = queryFactory
		.select(new QMemberDto(member.userName, member.age))
		.from(member)
		.fetch();

그럼 이렇게 가능하다.

참고로,
DTO로 조회할때는 페치조인처럼 연관된 엔티티나 컬렉션을 SQL한번에 조회할 수 있다. 지연로딩 즉시로딩 상관없이 무조건 한번의 쿼리로 연관된 엔티티나 컬렉션을 조회할 수 있다.

queryFactory
    .select(new QMemberTeamDto(
        member.id,
        member.username,
        team.id,
        team.name 
    ))
    .from(member)
    .leftJoin(member.team, team)
    .fetch();


동적쿼리

BooleanBuilder

@Test
public void 동적쿼리_BooleanBuilder() throws Exception {
        String usernameParam = "member1";
        Integer ageParam = 10;
        List<Member> result = searchMember1(usernameParam, ageParam);
        Assertions.assertThat(result.size()).isEqualTo(1);
}

private List<Member> searchMember1(String usernameCond, Integer ageCond) {
        BooleanBuilder builder = new BooleanBuilder();
        if (usernameCond != null) {
            builder.and(member.username.eq(usernameCond));
        }
        if (ageCond != null) {
    	    builder.and(member.age.eq(ageCond));
	}
        return queryFactory
                .selectFrom(member)
                .where(builder)
                .fetch();
}
  • where()에 booleanBuilder를 넣어준다.
  • builder.and()와 builder.or()를 사용해서 동적으로 조건을 넣을 수 있다.
  • builder는 내부적으로 첫 조건에는 AND/OR를 붙이지 않고, 이후 추가되는 조건부터 AND 또는 OR로 결합한다.
  • builder가 비어있으면 where절 자체를 만들지 않는다.
  • 참고로 builder.and(null) 이면 조건 자체를 만들지 않고 무시된다.
  • 참고로, builder.and(member.age.goe(null)) 는 builder.and(null) 이 아니라 WHERE age >= null 조건을 추가한다.
    그래서 where age >= null 과 같은 조건식이 만들어진다.
    null과의 비교는 IS NULL이나 IS NOT NULL과 같이 비교하는것만 가능하므로
    age >= null 처럼 하게되면 항상 모든 행이 다 false가 되어서 리턴되는 결과가 아무것도 없다.
builder.and(
    member.age.goe(20)
        .and(member.age.loe(30))
);

builder.and(
    member.username.eq("kim")
        .or(member.username.eq("lee"))
);
  • 이렇게 사용하면 WHERE (age >= 20 AND age <= 30) AND (username = 'kim' OR username = 'lee') 처럼 된다.

where 다중 파라미터

	@Test
    public void 동적쿼리_WhereParam() throws Exception {
        String usernameParam = "member1";
        Integer ageParam = 10;
        List<Member> result = searchMember2(usernameParam, ageParam);
        Assertions.assertThat(result.size()).isEqualTo(1);
    }

    private List<Member> searchMember2(String usernameCond, Integer ageCond) {
        return queryFactory
                .selectFrom(member)
                .where(usernameEq(usernameCond), ageEq(ageCond))
                //.where(allEq(usernameCond, ageCond))
                .fetch();
    }

    private BooleanExpression usernameEq(String usernameCond) {
        return usernameCond != null ? member.username.eq(usernameCond) : null;
    }

    private BooleanExpression ageEq(Integer ageCond) {
        return ageCond != null ? member.age.eq(ageCond) : null;
    }

    private BooleanExpression allEq(String usernameCond, Integer ageCond) {
        return usernameEq(usernameCond).and(ageEq(ageCond));
    }
  • member.username.eq("kim"); 자체는 BooleanExpression객체이다.
    즉, BooleanExpression expression = member.username.eq("kim"); 이다.
  • where(member.username.eq(usernameCond), member.age.eq(ageCond)) 에서,
    • where()의 파라미터로 여러개의 BooleanExpression객체나 null을 넣을 수 있다. null을 넣으면 해당 조건은 만들지 않고 무시된다.
    • 여러개의 BooleanExpression객체를 넣으면 이건 모두 무조건 AND로 결합된다.
      즉, WHERE member.username = 'member1' AND member.age = 10 처럼 된다.
      이게 아니라, WHERE member.username = 'member1' OR member.age = 10처럼 하고 싶으면,
      애초에 BooleanExpression을 파라미터로 넣을때 where(member.username.eq(usernameCond).or(member.age.eq(ageCond))) 처럼 해야한다.
      그럼, WHERE member.username = 'member1' OR member.age = 10 처럼 된다.

참고로,
.where(usernameEq(username), ageEq(age))와
.where(usernameEq(username).and(ageEq(age)))는 동일하다.
그래서 WHERE member.username = ? AND member.age = ? 실행되는 쿼리가 동일하다.
하지만, 컴마로 했을때에는 a 또는 b가 null 이면 자동 무시하고 NPE가 없음.
and로 했을때에는 usernameEq(username)가 null이면 null.and(...) 가 되어 NPE가 발생함.



SQL Function

REPLACE(원본문자열, 찾을문자열, 바꿀문자열)

List<String> result = queryFactory
                .select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})", member.username, "member", "M"))
                .from(member)
                .fetch();
  • username을 조회하는데, member문자열을 M으로 바꿔서 조회.

LOWER(문자열)

List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .where(member.username.eq(Expressions.stringTemplate("function('lower', {0})", member.username)))
                //.where(member.username.eq(member.username.lower())) // 이렇게 해도 동일하다.
                .fetch();
  • 소문자로 변경


스프링 데이터 페이징 활용1 - Querydsl 페이징 연동

  • 참고로, Spring Data JPA는
public interface MemberRepository extends JpaRepository<Member, Long> {
      Page<Member> findByAge(int age, Pageable pageable);
}

개발자는 메서드 시그니처만 정의하고 구현은 따로 하지않는다.
Spring Data JPA는 Repository 인터페이스에 메서드 시그니처만 정의하면, 메서드가 호출될 때 내부적으로 필요한 작업을 수행하고, 선언된 반환 타입에 맞게 값을 리턴해준다.

  • 참고로, Spring Data JPA는 Page<T> 처럼 반환 타입이 인터페이스인 경우에는, 내부적으로 return new PageImpl<>(content, pageable, total); 처럼 Page인터페이스의 구현체인 PageImpl을 리턴한다.
    • content : 현재 페이지 데이터
    • pageable : 페이지 정보
    • total : 전체 데이터 개수
public interface MemberRepositoryCustom {
    List<MemberTeamDto> search(MemberSearchCondition condition);

    Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable);
}

public class MemberRepositoryImpl implements MemberRepositoryCustom{
	
    .
	.
	.
    
    @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        QueryResults<MemberTeamDto> results = queryFactory
            .select(new QMemberTeamDto(
                member.id,
                member.username,
                member.age,
                team.id,
                team.name))
            .from(member)
            .leftJoin(member.team, team)
            .where(
                    usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe()))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .fetchResults();

        List<MemberTeamDto> content = results.getResults();

        long total = results.getTotal();

        return new PageImpl<>(content, pageable, total);
    }
}
  • 사용자 정의 인터페이스의 구현 클래스 안에서 메서드에 대해 구현을 할때에도 구현을 하고나서 반환타입에 맞게 값을 리턴해주면 된다.
    타입이 인터페이스인 경우에는, 구현체를 리턴하면 된다.
    return new PageImpl<>(content, pageable, total); 처럼 Page인터페이스의 구현체인 PageImpl을 리턴한다.
    • content : 현재 페이지 데이터
    • pageable : 페이지 정보
    • total : 전체 데이터 개수
  • 참고로 fetchResults()를 호출하면 페이지쿼리와 카운트쿼리가 나간다.

public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id,
                        member.username,
                        member.age,
                        team.id,
                        team.name))
                .from(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        long total = queryFactory
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .fetchCount();

        return new PageImpl<>(content, pageable, total);
    }
  • 데이터 조회 쿼리와 전체 카운트 쿼리를 분리하고 싶으면 이렇게 하면 된다.
  • 카운트쿼리에서는 어떤 데이터를 조회하는지가 중요한게 아니라 조건에 맞는 데이터의 '개수'가 중요하므로 select()에 DTO를 사용할 필요가 없다.
    Querydsl에서 fetchCount()를 호출하면 select()에 무엇을 넣었는지와 관계없이 내부적으로 count(루트 엔티티의 식별자) 형태의 쿼리가 실행된다.
    따라서 select(member)나 select(member.id)는 모두 count(member.id)로 처리된다.
  • 참고로 루트엔티티는 from절에 사용되는 쿼리의 기준이 되는 엔티티이다.


스프링 데이터 페이징 활용2 - CountQuery 최적화

public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
        List<MemberTeamDto> content = queryFactory
                .select(new QMemberTeamDto(
                        member.id,
                        member.username,
                        member.age,
                        team.id,
                        team.name))
                .from(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPAQuery<Member> countQuery = queryFactory
                .select(member)
                .from(member)
                .leftJoin(member.team, team)
                .where(usernameEq(condition.getUsername()),
                        teamNameEq(condition.getTeamName()),
                        ageGoe(condition.getAgeGoe()),
                        ageLoe(condition.getAgeLoe()));

        //return new PageImpl<>(content, pageable, total);
        return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchCount());
    }
  • PageRequest request = PageRequest.of(0, 10)에서 0은 offset이 아니라 페이지 번호다. 10은 페이지사이즈다.
    offset은 request.getOffset()으로 가져오며, 내부에서 페이지번호 * 페이지사이즈로 계산된다.
  • case1. 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈(한 페이지에 보여줄 데이터 개수)보다 작을때(=전체 데이터가 첫 페이지안에 모두 있을때)
    첫 페이지부터, 현재 페이지의 content.size() < pageable.getPageSize()인 경우에는 첫 페이지이자 마지막 페이지임을 알 수 있고, 전체 데이터의 개수를 알 수 있기 때문에, count쿼리가 필요없다.
    =====> 첫 페이지이면서 마지막페이지에서는 전체 데이터의 개수(content.size())를 알 수 있기 때문에, count쿼리가 필요없다.

  • case2. 마지막 페이지 일때(마지막 페이지의 offset + 마지막 페이지의 컨텐츠 사이즈를 더해서 전체 사이즈 구함)
    첫,중간 페이지들: 현재 페이지의 content.size() == pageable.getPageSize()인 경우에는 마지막 페이지인지 아닌지 모르기 때문에, 전체 데이터 개수와 다음 페이지 존재 여부를 알기 위해 count 쿼리 필요(현재 페이지의 offset + pageSize < total이면 다음 페이지 존재함.)
    마지막 페이지: 현재 페이지의 content.size() < pageable.getPageSize()인 경우에는 마지막 페이지임을 알 수 있고, 현재 페이지의 pageable.getOffset + content.size()로 전체 데이터 개수 계산 가능하기 때문에 count 쿼리 불필요
    =====> 마지막페이지에서는 pageable.getOffset() + content.size()로 전체 데이터의 개수를 알 수 있기 때문에, count쿼리가 필요없다.

  • PageableExecutionUtils.getPage()는 내부에서 조건을 판단한다. content.size() < pageSize 인 경우 내부적으로 count 쿼리 날리는것을 생략한다.
    그리고 결과적으로 PageableExecutionUtils.getPage()는 PageImpl<>(content, pageable, total)와 같은 PageImpl객체를 생성해서 리턴한다.
    => total에는 count쿼리를 날리든 안날리든 값이 있다. count쿼리를 날리든 안날리든 전체 데이터의 개수를 알 수 있기 때문에.


컨트롤러 개발

@GetMapping("/v2/members") 
public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) {
	return memberRepository.searchPageSimple(condition, pageable); 
}

여기서 Pageable의 파라미터의 값을 줄때,

  1. http://localhost:8080/v2/members?page=0&size=5 처럼 주면 된다.
    요청 파라미터에 page / size / sort가 있으면 PageRequest객체로 만들어서 파라미터에 넣어준다.
    page=0&size=5 처럼 주면 PageRequest.of(0,5)와 같은 객체가 생성되서 파라미터에 들어간다.
    참고로 sort까지 주면 ?page=0&size=5&sort=username,desc 처럼 하면 되고 PageRequest.of(0, 5, Sort.by("username").descending())와 같은 객체가 생성되서 파라미터에 들어간다.

  2. Pageable의 파라미터로 아예 값을 /v2/members 처럼 page나 size를 안주면, 기본값은 page는 0이고 size는 20이다. 즉, PageRequest.of(0, 20).



기타

  • JPQL / Querydsl는 조회 시 무조건 SELECT 쿼리를 날려 DB를 조회한다. 식별자(id)의 엔티티가 이미 영속성 컨텍스트에 있으면 DB에서 읽어온 값은 무시하고 기존 1차 캐시에 있는 영속 엔티티를 그대로 사용한다.
  • em.find()는 1차 캐시를 먼저 조회하고, 있으면 DB 조회를 하지 않는다.

0개의 댓글