본문 바로가기
Framework and Tool/JPA

JPA - 객체지향 쿼리 언어 - QueryDSL 검색조건, 페이징, 그룹

by ocwokocw 2021. 8. 16.

- 참조: 자바 ORM 표준 JPA 프로그래밍

- 검색조건

QueryDSL 에서 검색조건 where 절의 기본 사용법을 알아보자.

 

JPAQueryFactory query = new JPAQueryFactory(em);

QMember qMember = QMember.member;

List<Member> members = query.select(qMember)
		.from(qMember)
		.where(qMember.age.gt(20)
			.and(qMember.address.city.eq("City#2")))
		.orderBy(qMember.age.desc())
		.fetch();

members.forEach(member -> {
	System.out.println("Member age: " + member.getAge());
});

 

QueryDSL 환경 설정 편에서 사용하던 코드이다. and 조건을 추가하는 부분을 자세히 살펴보자. Java8 에서 제공하는 Predicate 처럼 and 메소드를 체이닝 메소드 형식으로 이어붙일 수 있어서 간편하다.

 

Hibernate: 
    /* select
        member1 
    from
        Member member1 
    where
        member1.age > ?1 
        and member1.address.city = ?2 
    order by
        member1.age desc */ select
            member0_.member_id as member_i1_6_,
            member0_.insert_datetime as insert_d2_6_,
            member0_.update_datetime as update_d3_6_,
            member0_.city as city4_6_,
            member0_.street as street5_6_,
            member0_.zipcode as zipcode6_6_,
            member0_.age as age7_6_,
            member0_.name as name8_6_ 
        from
            member member0_ 
        where
            member0_.age>? 
            and member0_.city=? 
        order by
            member0_.age desc
Member age: 32
Member age: 22

 

실행해서 생성되는 JPQL 을 보면 위와 같이 age 와 city 에 대한 조건이 잘 형성됨을 알 수 있다. 물론 위의 코드 처럼 간단한 and 뿐만 아니라 like 나 between 도 제공한다.

 

List<Member> members = query.select(qMember)
	.from(qMember)
	.where(qMember.age.between(10, 50)
		.and(qMember.name.startsWith("Name"))
		.and(qMember.address.city.eq("City#2")))
	.orderBy(qMember.age.desc())
	.fetch();

 

startWith 와 between 연산을 사용해서 생성된 쿼리는 아래와 같다.

 

Hibernate: 
    /* select
        member1 
    from
        Member member1 
    where
        member1.age between ?1 and ?2 
        and member1.name like ?3 escape '!' 
        and member1.address.city = ?4 
    order by
        member1.age desc */ select
            member0_.member_id as member_i1_6_,
            member0_.insert_datetime as insert_d2_6_,
            member0_.update_datetime as update_d3_6_,
            member0_.city as city4_6_,
            member0_.street as street5_6_,
            member0_.zipcode as zipcode6_6_,
            member0_.age as age7_6_,
            member0_.name as name8_6_ 
        from
            member member0_ 
        where
            (
                member0_.age between ? and ?
            ) 
            and (
                member0_.name like ? escape '!'
            ) 
            and member0_.city=? 
        order by
            member0_.age desc

- 결과조회

여태까지 예제에서 결과를 조회할 때 fetch() 만 사용했지만 QueryDSL 역시 다양한 메소드를 조회한다.

 

Mybatis 사용시 selectOne() 처럼 1 건의 QueryDSL 도 1 건의 데이터만 반환하는 fetchOne() 메소드를 지원한다. 

 

Member member = query.select(qMember)
	.from(qMember)
	.where(qMember.name.startsWith("Name"))
	.fetchOne();

.............

Exception in thread "main" com.querydsl.core.NonUniqueResultException: Only one result is allowed for fetchOne calls
	at com.querydsl.jpa.impl.AbstractJPAQuery.fetchOne(AbstractJPAQuery.java:258)
	at com.example.demo.JpaApplication.find(JpaApplication.java:90)
	at com.example.demo.JpaApplication.main(JpaApplication.java:32)

 

만약 1 건의 조회를 만족 시키지 못하면 위와 같이 Exception 을 내뱉는다. 2 건 이상이 검색되더라도 Exception 을 내지 않고 1 번째 데이터를 반환받고 싶다면 fetchFirst() 메소드를 사용하면 된다.


- 페이징과 정렬

페이징은 offset() 과 limit() 으로 인자를 주어 할 수 있다. limit 과 offset 으로 페이징을 지원하는 DB 에서 SQL 을 사용해봤다면 익숙할것이다. limit 으로는 한번에 몇 건의 데이터를 가져올 지 설정하며, offset 으로는 몇 건을 건너뛸지 정한다.

 

JPAQueryFactory query = new JPAQueryFactory(em);

QMember qMember = QMember.member;

List<Member> members = query.select(qMember)
		.from(qMember)
		.where(qMember.name.startsWith("Name"))
		.orderBy(qMember.name.asc())
		.offset(2).limit(2)
		.fetch();

members.forEach(member -> {
	System.out.println("Member name: " + member.getName());
});

 

이름순으로 정렬하여 2 개의 데이터를 건너뒤고 2 개의 데이터를 조회해보자. 우리가 기대한 결과는 Name#1~4 중 1~2를 건너뛰고 3~4를 조회하는것이다.

 

Hibernate: 
    /* select
        member1 
    from
        Member member1 
    where
        member1.name like ?1 escape '!' 
    order by
        member1.name asc */ select
            member0_.member_id as member_i1_6_,
            member0_.insert_datetime as insert_d2_6_,
            member0_.update_datetime as update_d3_6_,
            member0_.city as city4_6_,
            member0_.street as street5_6_,
            member0_.zipcode as zipcode6_6_,
            member0_.age as age7_6_,
            member0_.name as name8_6_ 
        from
            member member0_ 
        where
            member0_.name like ? escape '!' 
        order by
            member0_.name asc limit ? offset ?
Member Name: Name#3
Member Name: Name#4

 

실행해보면 우리의 예상대로 잘 동작한다.

 

단순히 데이터를 조회한다면 위와 같이 사용하면 된다. 하지만 시스템을 만들다보면 보통의 경우엔 전체 데이터가 몇 건인지를 알고 싶다는 요구사항이 많다. 페이징 정보를 알고 싶다면 fetchResults() 를 사용하면 된다. 

 

QueryResults<Member> memberResults = query.select(qMember)
		.from(qMember)
		.where(qMember.name.startsWith("Name"))
		.orderBy(qMember.name.asc())
		.offset(2).limit(2)
		.fetchResults();

long total = memberResults.getTotal();
long limit = memberResults.getLimit();
long offset = memberResults.getOffset();
List<Member> members = memberResults.getResults();

System.out.println("total: " + total + 
		", limit: " + limit +
		", offset: " + offset);

members.forEach(member -> {
	System.out.println("Name: " + member.getName());
});

 

fetchResults 는 QueryResults 형을 반환한다. QueryResults 에는 총 건수 total 과 사용한 offset, limit 정보가 들어있다. 그리고 조회된 데이터정보도 있다.

 

Hibernate: 
    /* select
        count(member1) 
    from
        Member member1 
    where
        member1.name like ?1 escape '!' */ select
            count(member0_.member_id) as col_0_0_ 
        from
            member member0_ 
        where
            member0_.name like ? escape '!'
Hibernate: 
    /* select
        member1 
    from
        Member member1 
    where
        member1.name like ?1 escape '!' 
    order by
        member1.name asc */ select
            member0_.member_id as member_i1_6_,
            member0_.insert_datetime as insert_d2_6_,
            member0_.update_datetime as update_d3_6_,
            member0_.city as city4_6_,
            member0_.street as street5_6_,
            member0_.zipcode as zipcode6_6_,
            member0_.age as age7_6_,
            member0_.name as name8_6_ 
        from
            member member0_ 
        where
            member0_.name like ? escape '!' 
        order by
            member0_.name asc limit ? offset ?
total: 4, limit: 2, offset: 2
Name: Name#3
Name: Name#4

 

코드를 실행해보면 조회 쿼리가 2번 실행된다. 하나는 Member 를 조회하기 위한 쿼리이며 다른 하나는 조건에 맞는 총 건수를 알기 위한 count() 쿼리가 수행된다.


- 그룹

QueryDSL 에서 Group By 사용법을 알아보자.

 

List<Tuple> members = query.select(qMember.address.city, qMember.count())
		.from(qMember)
		.where(qMember.name.startsWith("Name"))
		.groupBy(qMember.address.city)
		.fetch();
		
members.forEach(member -> {
	System.out.println("City: " + member.get(qMember.address.city) +
			", count: " + member.get(qMember.count()));
});

 

groupBy 사용법은 Criteria 와 비교해서 크게 특별한 점은 없다. 대신 Criteria 에서는 CriteriaBuilder 로 count 표현을 얻어야 하는것과는 달리 Q-Type 에서 qMember 에 대해 count() 를 곧바로 지원해준다.

댓글