Back-End/Study

JPAQueryFactory

yeonx 2022. 8. 1. 18:47
728x90

쿼리 만들기

QueryDSL을 사용하기 위해서는 먼저 QueryDSL이 제공하는 JPAQueryFactory 클래스의 인스턴스를 생성해야 한다.

JPAQuertFactory queryFactory = new JPAQueryFactory(em);
  • EntityManager를 ㅈ입하여 인스턴스를 생성해야 함

 

Spring Boot를 사용하면 다음과 같이 QueryFactory를 각 로직마다 주입해 줄 필요 없이 필드로 빼도 동시성 문제없이 각각 별도의 영속성 컨텐스트를 제공받아 작동함

@SpringBootTest
@Transactional
public class QueryDslBasicTest{
	@Autowired
    EntityManager em;
    
    JPAQueryFactory queryFactory;
    
    public void example(){
    	queryFactory = new JPAQueryFactory(em);
        
        ...
    }
}

이제 일반적인 JPQL과 QueryDSL을 비교해보자

public void startJPQL(){
	Member result = em.createQuery(
    	"select m from Member m " + "where m.username = :username",Member.class)
        .setParameter("username","member1")
        .getSingleResult();
}

public void startQueryDsl(){
	Member result = queryFactory
    				.select(member)
        			.from(member)
        			.where(member.username.eq("member1"))
   					.fetchOne();
}

위 두 코드는 동일한 JPQL로 데이터베이스에 조회 쿼리를 날린다. QueryDSL은 Criteria와 같이 JPAL 빌더 역할을 하기 때문에 결론적으로 JPQL을 생성하여 조회한다.

하지만 둘의 차이는 명확하다. JPQL은 문자로 작성이 되며 파라미터 바인딩을 직접 기입해줘야 한다. 반면에  QueryDSL은 쿼리 메소드와 쿼리 타입을 통해 자바 코드로 작성이 되며 파라미터 바인딩도 자동으로 처리한다.

하여

 QuerySAL은 쿼리에 오류가 있다면 컴파일 시점에서 오류를 발견할 수 있고, 자바 코드 작성 시에 사용할 수 있는 IDE의 막강한 기능들을 이용해 JPQL 쿼리를 생성할 수 있다.

 

QueryDSL이 제공하는 기본적인 쿼리 메소드

  • select()
  • from()
  • selectFrom() -> select 하는 엔티티와 from의 엔티티가 일치할 경우 합칠 수 있다.
  • where()
  • update()
  • set()
  • delect()

위와 같은 메서드를 체인으로 연결해 조합하여 마치 JPQL을 직접 짜듯이 메소드를 이용하여 쿼리를 짤 수 있다. 세세하게 들어가면 편의성이 높은 다양한 메서드들을 추가적으로 제공한다.

 


이제 다양한 예제를 통해 QueryDSL로 로직을 짠다. (Spring Boot와 Junit5를 활용한 테스트 코드로 예제를 작성)

 

검색 조건 쿼리

@Test
public void search(){
	Member result = queryFactory
    .selectFrom(member)
    .where(member.username.eq("member1")
    .and(member.age.eq(10)))
    .fetchOne();
	assertThat(result.getUsername()).isEqualTo("member1");
}
  • 검색 조건은 .and() .or() 메서드를 체인으로 연결할 수 있다. 

JPQL이 제공하는 모든 검색 조건을 QueryDSL이 아래와 같은 메서드로 지원한다.

member.username.eq("member1") // username = 'member1'
member.username.ne("member1") //username != 'member1'
member.username.eq("member1").not() // username != 'member1'
member.username.isNotNull() // 이름이 is not null
member.age.in(10,20) // age in (10,20)
member.age.notin(10,20) // age not in (10,20)
member.age.between(10,30) //between 10, 30
member.age.goe(30) // age >= 30
member.age.gt(30) // age > 30
member.age.loe(30) // age <= 30
member.age.lt(30) // age < 30
member.username.like("member%") // like 검색
member.username.contains("member") // like '%member%' 검색
member.username.startsWith("member") // like 'member%' 검색

 

and() 조건으로 파라미터 처리

@Test
public void searchAndParam(){
	Member result = queryFactory
    .selectFrom(member)
    .where( //and만 있는 경우엔 ,로 넣어도 된다.
    member.username.eq("member1"),member.age.eq(10))
    .fetchOne();
    
    assertThat(result.getUsername()).isEqualTo("member1");
}
  • where()에 파라미터로 검색 조건을 나열하면 and 조건으로 연결
  •  파라미터가 null 값으로 들어오면 무시 -> 메소드 추출을 활용하여 동적 쿼리를 깔끔하고 간결하게 구현

 

결과 조회

쿼리 완성 후 결과 조회 시 다음과 같은 메소드 이용

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    • 결과가 없으면 : null
    • 결과가 둘 이상이면 : com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne()
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount() : count 쿼리로 변경해서 count 수 조회

결과 조회 메서드 사용 예시 

@Test
pulic void getResult(){

	//List로 조회
    List<Member> result1 = queryFactory.selectFrom(member).fetch();
    
    //단건 조회
    Member result2 = queryFactory.selectFrom(member).where(member.username.eq("member1")).fetchOne();
    
    //==(...).limit(1).fetchOne()
    Member result3 = queryFactory.selectFrom(member)fetchFirst();
    
    //페이징 정보랑 같이 줌
    QueryResults<Member> result4 = queryFactory.selectFrom(member).fetchResults();
    
    result4.getTotal();
    
    List<Member> result4List = result4.getResults();
    
    //토탈 카운트 반환
    long result5 = queryFactory.selectFrom(member).fetchCount();

}

 

정렬

@Test
public void sort(){
	/*
    	회원 정렬 순서
        1. 회원 나이 내림차순 (desc)
        2. 회원 이름 올림차순 (asc)
        단, 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
    */
    em.persist(new Member(null,100));
    em.persist(new Member("member5",100));
    em.persist(new Member("member6", 100));
	
    List<Member> result = queryFactory.selectFrom(member).orderBy(member.age.desc(), member.username.asc().nullsLast().fetch())
	
    assertThat(result.get(0).getUsername()).isEqualTo("member5");
    assertThat(result.get(1).getUsername()).isEqualTo("member6");
    assertThat(result.get(2).getUsername()).isNull();
    
}
  • orderBy() 메서드를 사용하여 정렬 쿼리 생성
  • desc(), acs() : 일반 정렬
  • nullsLast(), nullsFirst() : null 데이터 순서 부여

 

페이징

단순 조회 건수 제한

@Test
public void paging(){
	List<Member> result = queryFactory.selectFrom(member)
    .orderBy(member.username.desc())
    .offset(1).limit(2).fetch();
    
    assertThat(result.size()).isEqualTo(2);
}

페이징 정보 받을 경우

@Test
public void paging2(){
	QueryResults<Member> result = queryFactory
    .selectFrom(member).orderBy(member.username.desc())
    .offset(1).limit(2).fetchResults();
    
    assertThat(result.getTotal()).isEqualTo(4);
    assertThat(result.getLimit()).isEqualTo(2);
    assertThat(result.getOffset()).isEqualTo(1);
    assertThat(result.getResults().size()).isEqualTo(2);
}
  • count 쿼리가 같이 실행되므로 카운터 쿼리를 따로 짜야 되는 경우에 분리해 사용
  • QueryResults는 다음과 같은 페이징 정보를 얻을 수 있는 메소드를 지원한다.
    • getTotal() : total count를 얻을 수 있다.
    • getLimit()
    • getOffset()

 

집합

집합 함수 사용 예시

@Test
public void aggregation(){
	List<Tuple> result = queryFactory
    .select(member.count(),member.age.sum(),member.age.avg(),
    member.age.max(),member.age.min())
    .from(member)
    .fetch()
    
    Tuple tuple = result.get(0);
    
    assertThat(tuple.get(member.count())).isEqualTo(4);
    assertThat(tuple.get(member.age.sum())).isEqualTo(52);
    assertThat(tuple.get(member.age.avg())).isEqualTo(13);
    assertThat(tuple.get(member.age.max())).isEqualTo(15);
    assertThat(tuple.get(member.age.min())).isEqualTo(10);
}
  • JPQL이 제공하는 모든 집합 함수를 제공한다.
    • count()
    • sum()
    • avg()
    • max()
    • min()

GroupBy 사용

@Test
public void group(){
	List<Tuple> result = queryFactory
    .select(team.name,member.age.avg()).from(member)
    .join(member.team,team)
    .groupBy(team.name)
    .fetch();
    
    Tuple teamA = result.get(0);
    Tuple teamB = result.get(1);
    
    assertThat(teamA.get(team.name)).isEqualTo("teamA");
    assertThat(teamB.get(team.name)).isEqualTo("teamB");
}
  • groupBy() 메서드를 체인 하여 사용할 수 있음
  • 그룹화된 결과를 제한하려면 having() 메소드를 체인하여 사용하면 됨

 

참고 : https://ykh6242.tistory.com/107