@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
@Id @GeneratedValue
@Column(name = "member_id")
private Long id;
private String username;
private int age;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "team_id")
private Team team;
public Member(String username) {
this( username, 0);
}
public Member(String username, int age) {
this(username, age, null);
}
public Member(String username, int age, Team team) {
this.username = username;
this.age = age;
if (team != null) {
changeTeam(team);
}
}
private void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "name"})
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
List<Member> members = new ArrayList<>();
public Team(String name) {
this.name = name;
}
}
@Test
public void startJPQL() {
//member1을 찾아라.
Member findMember = em.createQuery("select m from Member m where m.username = : username", Member.class)
.setParameter("username", "member1")
.getSingleResult();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
// QueryDSL은 컴파일 타임에 오류를 잡을 수 있다.
// 파라미터 바인딩을 자동으로 해결해 준다.
@Test
public void startQuerydsl() {
// JPAQueryFactory queryFactory = new JPAQueryFactory(em); // 필드로 가져감 (멀티스레드에서도 문제 없게 설계되어있다.)
// QMember m = new QMember("m");
// QMember는 static으로 지정 가능 (권장)
Member findMember = queryFactory
.select(member) // QMember.member
.from(member)
.where(member.username.eq("member1")) // 파라이멑 바인딩 처리
.fetchOne();
assertThat(findMember.getUsername()).isEqualTo("member1");
}
QMember qMember = new QMember("m"); // 별칭 직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용

Querydsl은 결국 jpql 빌더 역할을 하는데 jpql 문법을 로그로 확인하고 싶다면 use_sql_comments를 추가하면된다.
💡 나머지 기본문법 코드는 너무 많은 관계상 github의 코드를 확인하고 중요한 부분만 정리해놓겠다.
/**
* 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회
* JPQL: select m, t from Member m left join m.team t on t.name = 'teamA'
*/
@Test
public void join_on_filtering() {
List<Tuple> result = queryFactory
.select(member, team)
.from(member)
.join(member.team, team)
//.on(team.name.eq("teamA"))
.where(team.name.eq("teamA"))
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple = " + tuple);
}
}
@Test
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(20, 30, 40);
}
com.querydsl.jpa.JPAExpresions 사용member, memberSub으로 다른 alias 사용💡 from절의 서브쿼리 한계
JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않는다.
당연히 Querydsl도 지원하지 않는다. 하이버네이트 구현체를 사용하면 select절의 서브쿼리는 지원한다. Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원한다.
[해결방안]
1. 서브쿼리를 join으로 변경한다. (가능한 상황도 있고, 불가능한 상황도 있다.)
2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다.
3. nativeSQL을 사용한다.
서브쿼리는 높은 확률로 join으로 바꿀 수 있다.
성능이 엄청 중요한 시스템이 아닌이상 query를 두 번 이상 사용해도 된다.(상황에 따라 다름)
SQL은 데이터를 가져오는 것에 집중을하고 필요하면 애플리케이션에서 로직을 태우는 것이 좋다.
(Query를 너무 복잡하게 할 필요가 없다. (물론 WHERE와 GROUP BY를 통해 잘라내는 것도 중요하다.))
💡 하이버네이트 6부터는 From절에서의 서브쿼리를 지원한다고 한다.
아직은 공식적으로 지원해주지는 않고 있는 것으로 보여 서드파티를 적용해야 할 것 같다고 한다
프로젝션: select 대상 지정
@Test
public void simpleProjection() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
@Test
public void tupleProjection() {
// Tuple -> package com.querydsl.core
// Tuple을 Repository 계층 안에서 사용하는 것은 괜찮은데 Service나 Cotroller까지 넘어가는 것은 좋은 설계가 아니다.
// 하부 기술을 다른 곳에 노출 시키면 변경이 힘들다.
List<Tuple> result = queryFactory
.select(member.username, member.age)
.from(member)
.fetch();
for (Tuple tuple : result) {
String username = tuple.get(member.username);
Integer age = tuple.get(member.age);
System.out.println("username = " + username);
System.out.println("age = " + age);
}
}
@Test
public void findDtoByJPQL() {
// 순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야함
// DTO의 package 이름을 다 적어줘야해서 지저분함
// 생성자 방식만 지원함
List<MemberDto> result = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
.getResultList();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
💡 Querydsl 빈 생성
: 결과를 DTO 반환할 때 사용
다음 3가지 방법 지원
- 프로퍼티 접근
- 필드 직접 접근
- 생성자 사용
@Test
public void findDtoBySetter() {
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
위의 방법을 사용하려면 해당 DTO에 기본생성자가 있어야 한다.
-> Querydsl이 해당 DTO를 만든 다음 그 값에 Set을 해야하는데 기본생성자가 없다면 오류가 뜬다.
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
@Test
public void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
// QueryProjection을 사용하면 컴파일 에러로 확인이 가능하다.
// 단점?
// MemberDto 자체가 QueryDSL에 의존성을 가지게 된다.
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
@Test
public void dynamicQuery_BooleanBuilder() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember1(usernameParam, ageParam);
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();
}
@Test
public void dynamicQuery_WhereParam() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.size()).isEqualTo(1);
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
// where 가 만약 null이라면 값이 무시가 된다.
.where(usernameEq(usernameCond), ageEq(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;
}
BooleanBuilder보다 Where 다중 파라미터 사용이 더 깔끔하게 쿼리를 작성할 수 있다. (가독성이 높다.) private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
@Test
public void bulkUpdate() {
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
em.flush();
em.clear();
}
💡 벌크 연산은 조심해야될게 있다.
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 바로 Query가 날라간다 (DB의 상태와 영속성 컨텐스트의 상태가 달라진다)
-> 고로 벌크 연산 수행 시 영속성 컨텍스트를 flush 후 초기화 하도록 하자
이번 포스팅에서는 Querydsl의 문법들을 정리해봤다. 다음 포스팅에서는 순수 JPA와 Querydsl Spring data JPA와 Querydsl에 대해서 알아보도록 하자.