[Querydsl] JPQL과 Querydsl의 비교, 기본 문법 1탄
📌 Topic
- JPQL vs Querydsl
- 기본 Q-Type 활용
- 검색 조건 쿼리
- 결과 조회
- 정렬
- 페이징
- 집합
01. JPQL vs Querydsl
이번 시간에는 JPQL과 Querydsl의 차이에 대해 알아보는 시간을 갖는다
01-1. QueryDslBasicTest 테스트 작성
// JPQL 사용
@Test
@DisplayName("단일 회원 검색 - JPQL 사용")
public void findMemberByUserNameWithJPQL() {
//member1 을 찾아라.
String qlString =
"select m from Member m " +
"where m.userName = :userName";
Member findMember = em.createQuery(qlString, Member.class)
.setParameter("userName", "김영민")
.getSingleResult();
assertThat(findMember.getUserName()).isEqualTo("김영민");
}
- JPQL은 쿼리를 직접 작성할 수 있다
- 하지만 JPQL은 문자열이기 때문에 컴파일 시점에 문제를 잡을 수 없다
- 또한 파라미터 바인딩도 위와 같이 직접 선언을 해주어야 한다
// Querydsl 사용
@Test
@DisplayName("단일 회원 검색 - Querydsl 사용")
public void findMemberByUserNameWithQuerydsl() {
// 01. JPAQueryFactory 생성
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
QMember m = new QMember("m");
Member findMember = queryFactory
.select(m)
.from(m)
.where(m.userName.eq("김영민"))
.fetchOne();
assertThat(findMember.getUserName()).isEqualTo("김영민");
}
- JPAQueryFactory를 만들 때 EntityManager 객체를 생성자에 전달
- QMember는 변수명에다 별칭(alias)를 주는게 좋다, 안써도 상관 없음
- 나머지 부분은 일반적인 SQL의 형태와 동일하다
- JPQL은 문자로 작성, Querydsl은 자바 코드로 작성이 되는 장점이 있다
- 즉, 자바 컴파일 시점에 요류를 잡아낼 수 있다
- 파라미터 바인딩을 자동으로 수행 해준다
@SpringBootTest
@Transactional
@DisplayName("JPQL <> Querydsl 테스트")
public class QueryDslBasicTest {
@PersistenceContext
EntityManager em;
JPAQueryFactory queryFactory;
@BeforeEach
public void before() {
queryFactory = new JPAQueryFactory(em);
//..
}
//...
}
- JPAQueryFactory는 맴버 변수로 빼서 사용이 가능하다
- 동시성 문제가 발생하지 않도록 내부 설계가 구현 되어 있음
- @BeforeEach가 붙어있는 before 메서드에서 초기화를 진행함
02. 기본 Q-Type 활용
이번 시간에는 기본 Q클래스 인스턴스를 이용하는 3가지 방법을 알아보자
기본 Q클래스를 사용하는 방법은 아래와 같이 3가지가 존재한다.
QMember qMember = new QMember("m"); // 별칭 직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용
QMember qMember = member; // static import 사용
02-1. Q Class 확인을 위한 테스트 코드 작성
@Test
@DisplayName("Querydsl에서 Q Class를 사용하는 방법 테스트")
public void createQuerydslQueueClass() {
// 기본 인스턴스 사용
QMember m1 = member;
// QMember.member -> member ==> static import
Member findMember = queryFactory
.select(member)
.from(member)
.where(member.userName.eq("김영민"))
.fetchOne();
assertThat(findMember.getUserName()).isEqualTo("김영민");
}
- 위에서는 static import를 통해 QMember 사용
- 내부적으로 들어가보면 static final로 선언이 되어있음
- Querydsl은 결국 JPQL의 빌더(Builder) 역할을 수행한다
- 즉, Querydsl로 작성한 코드는 JPQL이 된다는 의미다
03. 검색 조건 쿼리
이번 시간에는 Querydsl의 where절에 사용되는 검색 조건에 대해 알아보자
03-1. 테스트 코드 작성
@Test
@DisplayName("Querydsl의 where절에 사용되는 구문 테스트")
public void search() throws Exception {
// 이름이 임수현이 아닌 모든 회원 조회
List<Member> findMember_1 = queryFactory
.selectFrom(member)
.where(member.userName.ne("임수현"))
.fetch(); // ne
assertThat(findMember_1).hasSizeGreaterThan(0);
// 이름이 임수현이고, 나이가 29인 단일 회원 조회
Member findMember_2 = queryFactory
.selectFrom(member)
.where(member.userName.eq("임수현")
.and(member.age.eq(29)))
.fetchOne(); // eq
assertThat(findMember_2.getUserName()).isEqualTo("임수현");
// 나이가 20 ~ 29 사이에 존재하는 회원 조회
List<Member> findMember_3 = queryFactory
.select(member)
.from(member)
.where(member.age.between(20, 29))
.fetch();
assertThat(findMember_3).isNotEmpty();
assertThat(findMember_3).hasSize(3); // expect) 3명의 회원
for (Member member : findMember_3) {
System.out.println(">>>>>>>> member.getUserName = " + member.getUserName() + ", age = " + member.getAge());
}
}
이전과 달라진 부분은 from절이 select절과 합쳐지고
where절에 and 조건이 추가되었다.
03-2. JPQL이 제공하는 모든 검색 조건 제공
member.userName.eq("김철수") // userName == '김철수'
member.userName.ne("김철수") // userName != '김철수'
member.userName.eq("김철수").not() // userName != '김철수'
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)
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("김철%") // like 검색
member.userName.contains("member") // like '%member%' 검색
member.userName.startsWith("member") // like 'member%' 검색
- Querydsl에서 제공하는 대표적인 조건
- 위에서 언급하였지만, JPQL에서 지원되는 모든 부분을 지원한다
03-3. 검색 조건 AND를 다른 방식으로 사용
// AS-IS
@Test
@DisplayName("Querydsl의 where절에 사용되는 구문 테스트 - 기존 and 사용 방식")
public void searchAndParam() throws Exception {
Member findMember = queryFactory
.selectFrom(member)
.where(member.userName.eq("김영민").and(member.age.eq(30))) // 기존 and 사용 방식
.fetchOne();
assertThat(findMember.getUserName()).isEqualTo("김영민");
assertThat(findMember.getAge()).isEqualTo(30);
}
// TO-BE
@Test
@DisplayName("Querydsl의 where절에 사용되는 구문 테스트 - and를 다른 방식으로 사용")
public void searchAndParam() throws Exception {
Member findMember = queryFactory
.selectFrom(member)
.where(
member.userName.eq("김영민"), // 변경된 and 사용 방식
member.age.eq(30)
)
.fetchOne();
assertThat(findMember.getUserName()).isEqualTo("김영민");
assertThat(findMember.getAge()).isEqualTo(30);
}
- and, or는 위와 같이 ‘,’로 구분하여 사용이 가능하다
- 동적 쿼리 사용시에 유용하게 활용이 가능한 부분이다
04. 결과 조회
이번 시간에는 Querydsl의 결과 조회 메서드에 대해 정리 해보자
- fetch()
- 리스트 조회, 데이터 없을 시 빈 리트스 반환
- 빈 리스트를 반환하기에 불필요한 IF ~ ELSE 사용 피할 수 있음
- fetchOne()
- 단 건 조회
- 결과가 없으면 null
- 결과가 둘 이상이면 com.querydsl.core.NoUniqueResultException
- fetchFirst()
- limit(1).fetchOne()
- fetchResults()
- 페이징 정보 포함, total count 쿼리 추가 실행
- fetchCount()
- count 쿼리로 변경해서 count 수 조회
04-1. 결과 조회 테스트 코드 작성
@Test
@DisplayName("결과 조회 테스트")
public void resultFetchTest() throws Exception {
/*List<Member> memberList = queryFactory
.selectFrom(member)
.fetch();
Member findMember = queryFactory
.selectFrom(member)
.fetchOne();
Member firstMember = queryFactory
.selectFrom(member)
.fetchFirst();*/
QueryResults<Member> result = queryFactory
.selectFrom(member)
.fetchResults();
result.getTotal(); // count query
List<Member> results = result.getResults(); // contents query
long total = queryFactory
.selectFrom(member)
.fetchCount();
}
fetchResults 같은 경우에 페이징 쿼리가 복잡해지면
컨텐츠 쿼리와 total 쿼리가 다른 경우가 존재한다.
만약 복잡한 페이징 처리가 필요하다면
컨텐츠 쿼리와 total 쿼리를 분리하여 2번의 쿼리를 날리는 것이 좋다.
- fetch query
- fetchOne query
- fetchFirst query
- fetchResults query
05. 정렬
@Test
@DisplayName("정렬 테스트 진행")
public void test() throws Exception {
// 회원 이름 순으로 내림차순, 나이 순으로 오름차순
em.persist(new Member(null, 100));
em.persist(new Member("김영민", 100));
em.persist(new Member("정주리", 100));
//querydsl 작성
List<Member> memberList = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.asc(), member.userName.desc().nullsLast())
.fetch();
System.out.println(">>>> memberList = " + memberList.toString());
Member member = memberList.get(0);
Member member1 = memberList.get(1);
Member member2 = memberList.get(2);
assertThat(member.getUserName()).isEqualTo("정주리");
assertThat(member1.getUserName()).isEqualTo("김영민");
assertThat(member2.getUserName()).isNull();
}
정렬의 경우 기존 SQL와 동일하게 orderBy 메서드를 사용하여 정렬 가능.
06. 페이징
@Test
@DisplayName("일반 페이징 테스트 진행")
public void paging() throws Exception {
// Team 3개, Member 5명
// Querydsl의 offset은 0부터 시작이 된다
// offset: 시작 인덱스(row)
// limit: 조회 개수
List<Member> memberListByPaging = queryFactory // 김영민, 원영식, 임수현, 박진우, 김진엽
.selectFrom(member)
.orderBy(member.age.desc())
.offset(0)
.limit(5)
.fetch();
System.out.println(">>>> memberListByPaging = " + memberListByPaging.toString());
assertThat(memberListByPaging.size()).isEqualTo(5);
}
@Test
@DisplayName("Count 포함 페이징 테스트 진행")
public void pagingWithCount() throws Exception {
QueryResults<Member> queryResult = queryFactory
.selectFrom(member)
.orderBy(member.age.desc())
.offset(0)
.limit(10)
.fetchResults();
assertThat(queryResult.getResults().size()).isEqualTo(5); // 전체 회원 사이즈
assertThat(queryResult.getLimit()).isEqualTo(10);
assertThat(queryResult.getTotal()).isEqualTo(5);
assertThat(queryResult.getOffset()).isEqualTo(0);
}
Querydsl은 offset, limit을 통해 페이징을 지원한다
- offset
- 페이지 인덱스(Row) 지정
- limit
- 가져올 데이터 개수 지정
select
member0_.member_id as member_i1_1_,
member0_.age as age2_1_,
member0_.team_id as team_id4_1_,
member0_.user_name as user_nam3_1_
from
member member0_
order by
member0_.user_name desc limit ? offset ?
실제로 DB에 요청된 쿼리는 위와 같다
07. 집합
@Test
@DisplayName("집합 테스트")
public void aggregation() throws Exception {
// Querydsl Tuple 타입
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(5);
assertThat(tuple.get(member.age.sum())).isEqualTo(147);
assertThat(tuple.get(member.age.avg())).isEqualTo(29.4);
assertThat(tuple.get(member.age.max())).isEqualTo(33);
assertThat(tuple.get(member.age.min())).isEqualTo(27);
}
Tuple을 사용하는 이유는 다양한 타입을 받아오기 때문이다.
하지만 실무에서는 DTO를 통해 SELECT절의 값을 받아온다.
select
count(member0_.member_id) as col_0_0_,
sum(member0_.age) as col_1_0_,
avg(cast(member0_.age as double)) as col_2_0_,
max(member0_.age) as col_3_0_,
min(member0_.age) as col_4_0_
from
member member0_
위 테스트코드가 실행된 쿼리는 위와 같다.
@Test
@DisplayName("GroupBy 테스트")
public void groupBy() throws Exception {
// 팀의 이름과 각 팀의 평균 연령을 구해라.
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("데이터 플랫폼 팀");
assertThat(teamA.get(member.age.avg())).isEqualTo(31.5);
assertThat(teamB.get(team.name)).isEqualTo("인프라 팀");
assertThat(teamB.get(member.age.avg())).isEqualTo(28);
}
GroupBy 테스트 코드
댓글남기기