쿼리를 자바 코드로 적을 수 있도록 해줌.
SQL 스타일로 작성하기 때문에 복잡한 쿼리도 쉽게 작성이 가능함.
동적쿼리까지 쉽게 작성이 가능함.
프로젝트 환경설정
프로젝트 생성
QueryDSL 설정
build.gradle에 설정해야 함.
강의와는 다르게 설정해야 함. 현재 스프링부트 2점대 LTS가 2.7.6까지 올라왔기 때문.
2.6 이상에서는 이렇게 설정해야 함.
buildscript { ext { queryDslVersion = "5.0.0" } } plugins { id 'java' id 'org.springframework.boot' version '2.7.6' // querydsl plugins 추가 id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'study' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' //queryDsl 추가 implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" implementation "com.querydsl:querydsl-apt:${queryDslVersion}" compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.h2database:h2' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() } //querydsl 추가 시작 // querydsl 사용할 경로 설정 def querydslDir = "$buildDir/generated/querydsl" // JPA 사용 여부와 사용할 경로를 설정 querydsl { jpa = true querydslSourcesDir = querydslDir } // build 시 사용할 sourceSet 추가 sourceSets { main.java.srcDir querydslDir } // querydsl이 compileClassPath를 상속하도록 설정 configurations { compileOnly { extendsFrom annotationProcessor } querydsl.extendsFrom compileClasspath } //querydsl 컴파일시 사용할 옵션 설정 compileQuerydsl { options.annotationProcessorPath = configurations.querydsl }
그리고 플러그인 추가와 디펜던시 안에 jpa, apt 추가할 때 큰 따옴표로 넣어야 buildScript 변수를 읽을 수 있다.
라이브러리 살펴보기
querydsl-apt ⇒ Q 클래스 생성하는 라이브러리
querydsl-jpa ⇒ jpa와 결합해서 쓰기 위함.
JPQL vs QueryDSL
@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"); } @Test public void startQuerydsl() { JPAQueryFactory queryFactory = new JPAQueryFactory(em); QMember m = new QMember("m"); Member findMember = queryFactory .select(m) .from(m) .where(m.username.eq("member1")) .fetchOne(); assertThat(findMember.getUsername()).isEqualTo("member1"); }
QueryDSL의 Q-Type 활용
기본적으로 Q 클래스 안에 static final로 entity가 들어가 있으므로 static import해서 쓰는 것을 권장함.
QMember qMember = new QMember("m"); //별칭 직접 지정 QMember qMember = QMember.member; //기본 인스턴스 사용 // 기본 인스턴스 + static import (권장) import static study.querydsl.entity.QMember.*; @Test public void startQuerydsl() { Member findMember = queryFactory .select(member) .from(member) .where(member.username.eq("member1")) .fetchOne(); assertThat(findMember.getUsername()).isEqualTo("member1"); }
쿼리 검색 조건
JPQL이 지원하는 것은 모두 지원한다.
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%’ 검색 ... ... @Test public void search() { Member findMember = queryFactory .selectFrom(member) .where(member.username.eq("member1").and(member.age.eq(10))) .fetchOne(); assertThat(findMember.getUsername()).isEqualTo("member1"); } //.and 조건을 파라미터로 처리가 가능하다. @Test public void search() { List<Member> result1 = queryFactory .selectFrom(member) .where(member.username.eq("member1"), member.age.eq(10)) .fetch(); assertThat(result1.size()).isEqualTo(1); }
where()에 파라미터로 검색조건을 추가하면 AND 조건이 추가됨.
이 때 조건 중 null 값이 있다면 무시함. → 메서드 추출을 활용하여 동적 쿼리를 깔끔하게 만들 수 있음.
결과 조회
- fetch(): 리스트 조회, 데이터 없으면 빈 리스트 반환
- fetchOne(): 단 건 조회
- 결과가 없으면: null
- 결과가 둘 이상이면: NonUniqueResultException
- fetchFirst(): limit(1).fetchOne()
- fetchResults(): 페이징 정보 포함, total count 쿼리 추가 실행 (지금 버전의 doc에 의하면 fetch()를 사용하도록 권장하고 있음. 어차피 count 또한 List의 size로 알 수 있음.)
- fetchCount(): count 쿼리로 변경해서 count 수 조회
정렬
List<Member> result = queryFactory.selectFrom(member) .where(member.age.eq(100)) .orderBy(member.age.desc(), member.username.asc().nullsLast()) .fetch();
.desc(), .asc() 로 정렬을 할 수 있다. null의 경우엔 nullsLast, nullsFirst를 통해서 정렬시킬 수 있다.
페이징
sql 하듯이 .offset, .limit 사용하면 된다. (복잡하지 않은 경우는 spring-data-jpa의 page 기능을 사용하는 것이 더 좋을 듯 하다.)
@Test public void paging1() { queryFactory .selectFrom(member) .orderBy(member.username.desc()) .offset(1) .limit(2) .fetch(); }
집합 (집계함수들)
@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(); Tupletuple = result.get(0); assertThat(tuple.get(member.count())).isEqualTo(4); assertThat(tuple.get(member.age.sum())).isEqualTo(100); assertThat(tuple.get(member.age.avg())).isEqualTo(25); assertThat(tuple.get(member.age.max())).isEqualTo(40); assertThat(tuple.get(member.age.min())).isEqualTo(10); }
Tuple 이라는 타입으로 가져오는 것이 특이한 점임.
@Test public void group() throwsException{ List<Tuple> result = queryFactory .select(team.name, member.age.avg()) .from(member) .join(member.team, team) .groupBy(team.name) .fetch(); TupleteamA = result.get(0); TupleteamB = result.get(1); assertThat(teamA.get(team.name)).isEqualTo("teamA"); assertThat(teamA.get(member.age.avg())).isEqualTo(15); assertThat(teamB.get(team.name)).isEqualTo("teamB"); assertThat(teamB.get(member.age.avg())).isEqualTo(35); }
이런 식으로 groupBy 또한 사용가능 하고 뒤에 .having()을 넣어서 그룹 조건을 걸 수도 있다.
JOIN
@Test public void join() { List<Member> result = queryFactory .selectFrom(member) .join(member.team, team) .where(team.name.eq("teamA")) .fetch(); assertThat(result).extracting("username").containsExactly("member1", "member2"); } /** * 세타 조인 * 회원의 이름이 팀 이름과 같은 회원 조회 */ @Test public void theta_join() { em.persist(new Member("teamA")); em.persist(new Member("teamB")); List<Member> result = queryFactory .select(member) .from(member, team)//모든 member와 team을 다 조인시켜 버림 .where(member.username.eq(team.name)) .fetch(); assertThat(result) .extracting("username") .containsExactly("teamA", "teamB"); }
- join(), innerJoin(): inner Join
- leftJoin(): left outer join
- rightJoin(): right outer join
- JPQL의 on과 fetch 조인 제공.
JOIN - ON
/* 회원과 팀을 조인하면서, 팀 이름이 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) .leftJoin(member.team, team).on(team.name.eq("teamA")) .fetch(); for (Tuple tuple : result) { System.out.println("tuple = " + tuple); } // 쿼리 날라가는 모습 select member0_.member_id as member_i1_1_0_, team1_.team_id as team_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_ left outer join team team1_ on member0_.team_id=team1_.team_id and ( team1_.name=? ) // 사실 inner join일 때는 on 부분을 where 로 처리해도 같은 결과가 나온다. // left join 인 경우에는 on을 사용할 수밖에 없다. @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); } } // 쿼리 select member0_.member_id as member_i1_1_0_, team1_.team_id as team_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_.team_id where team1_.name=?
연관관계가 없는 테이블을 조인하고 싶을 때 on 사용
@Test public void join_on_no_relation() { em.persist(new Member("teamA")); em.persist(new Member("teamB")); em.persist(new Member("teamC")); List<Tuple> result = queryFactory .select(member, team) .from(member) .leftJoin(team).on(member.username.eq(team.name)) //member 이름과 team의 이름이 같은 경우만 조인 .fetch(); for (Tuple tuple : result) { System.out.println("tuple = " + tuple); } } //leftJoin 안에 member.team을 넣으면 id 값이 같은 것만 조인하고 안 넣으면 on 절의 조건만 가지고 조인한다.
페치 조인
SQL이 제공하는 기능은 아니고 SQL 조인을 활용하여 연관된 엔티티를 SQL 한 번에 조회하는 기능이다.
주로 성능 최적화에 사용하는 방법임.
@Test public void fetchJoinNo() { em.flush(); em.clear(); Member findMember = queryFactory .selectFrom(member) .where(member.username.eq("member1")) .fetchOne(); }
위의 상태로 실행해보면 member와 team은 LAZY 관계이기 때문에 member만 조회해 온다.
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_.username=?
@Test public void fetchJoinNo() { em.flush(); em.clear(); Member findMember = queryFactory .selectFrom(member) .where(member.username.eq("member1")) .fetchOne(); // 영속성 컨텍스트에 로드 되었는지 실제로 확인하는 방법 boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam()); assertThat(loaded).as("페치 조인 미적용").isFalse(); }
이렇게 테스트를 해보면 team이 로드되지 않아서 false가 나오기 때문에 테스트가 통과된다.
@Test public void fetchJoinUse() { em.flush(); em.clear(); Member findMember = queryFactory .selectFrom(member) .join(member.team, team).fetchJoin() .where(member.username.eq("member1")) .fetchOne(); // 영속성 컨텍스트에 로드 되었는지 실제로 확인하는 방법 boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam()); assertThat(loaded).as("페치 조인 적용").isTrue(); }
이제 join 뒤에 fetchJoin을 넣어서 페치 조인을 사용하면 LAZY 였던 team을 한꺼번에 조회해 오기 때문에 true가 된다.
select member0_.member_id as member_i1_1_0_, team1_.team_id as team_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_.team_id where member0_.username=?
쿼리가 이렇게 바뀌어서 나가기 때문이다.
서브 쿼리
com.querydsl.jpa.JPAExpressions 사용
/** * 나이가 가장 많은 회원 조회 */ @Test public void subQuery() { // 서브 쿼리를 하고 싶을 때는 alias가 겹치면 안되기 때문에 서브 쿼리 안에 들어갈 member 엔티티를 새로 생성해준다. 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); } 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_ ) /** * 나이가 평균보다 많은 회원 조회 */ @Test public void subQueryGoe() { // 서브 쿼리를 하고 싶을 때는 alias가 겹치면 안되기 때문에 서브 쿼리 안에 들어갈 member 엔티티를 새로 생성해준다. QMember memberSub = new QMember("memberSub"); List<Member> result = queryFactory .selectFrom(member) .where(member.age.goe( JPAExpressions .select(memberSub.age.avg()) .from(memberSub) )) .fetch(); assertThat(result).extracting("age") .containsExactly(30, 40); } 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 avg(cast(member1_.age as double)) from member member1_ ) /** * 나이가 특정 조건 그룹 안에 있는 회원 조회 */ @Test public void subQueryIn() { // 서브 쿼리를 하고 싶을 때는 alias가 겹치면 안되기 때문에 서브 쿼리 안에 들어갈 member 엔티티를 새로 생성해준다. 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); } 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>? )
주의할 점은 서브쿼리 안에 들어가는 Q클래스의 alias가 중복되면 안되기 때문에 같은 Member의 서브쿼리를 날릴 때는 새로 하나 선언해주고 사용해야 한다는 것이다.
@Test public void selectSubQuery() { QMember memberSub = new QMember("memberSub"); //JPAExpressions 또한 static import 가능하다. List<Tuple> result = queryFactory .select(member.username, select(memberSub.age.avg()) .from(memberSub)) .from(member) .fetch(); for (Tuple tuple : result) { System.out.println("tuple = " + tuple); } }
JPAExpressions static import 가능하다.
*** from 절의 서브쿼리 한계
JPQL의 한계로 인해 from 절의 서브쿼리는 지원하지 않는다.
→ from 절의 서브쿼리를 해결하고 싶다면
- 서브쿼리를 join으로 변경. (불가능한 상황도 있음.)
- 애플리케이션에서 쿼리를 2번 분리해서 실행.
- nativeSQL을 사용한다.
Case 문
- select, where 절에서 사용가능.
@Test public void basicCase() { List<String> result = queryFactory .select(member.age .when(10).then("열 살") .when(20).then("스무 살") .otherwise("기타")) .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(0, 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); } }
복잡한 조건의 경우엔 CaseBuilder()를 사용한다.
*** 과연 써야 하는 것인가는 생각을 해봐야 한다. 이런 내용들은 사실 로직으로 처리하는 것이 낫다.
상수, 문자 더하기
- 상수를 넣어야 하는 경우에는 Expressions.constant()를 사용
- 문자 더하기는 concat
@Test public void constant() { List<Tuple> result = queryFactory .select(member.username, Expressions.constant("A")) .from(member) .fetch(); for (Tuple tuple : result) { System.out.println("tuple = " + tuple); } } select member0_.username as col_0_0_ from member member0_ //result tuple = [member1, A] tuple = [member2, A] tuple = [member3, A] tuple = [member4, A] @Test public void concat() { //{username}_{age} 같은 걸 만들고 싶을 때 List<String> result = queryFactory .select(member.username.concat("_").concat(member.age.stringValue())) .from(member) .where(member.username.eq("member1")) .fetch(); for (String s : result) { System.out.println("s = " + s); } } select ((member0_.username||?)||cast(member0_.age as character varying)) as col_0_0_ from member member0_ where member0_.username=? s = member1_10
.stringValue 부분이 중요하다.
문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있고 ENUM 타입을 문자로 변환할 때도 자주 사용한다.
중급 문법
프로젝션과 결과 반환
@Test public void tupleProjection() { 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); }
select 결과물을 여러 타입으로 따로따로 변환하고 싶을 때는 Tuple을 사용한다.
DTO로 결과 반환
// 순수 JPQL로 작성하는 경우 @Test public void findDtoByJPQL() { 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); } select member0_.username as col_0_0_, member0_.age as col_1_0_ from member member0_ memberDto = MemberDto(username=member1, age=10) memberDto = MemberDto(username=member2, age=20) memberDto = MemberDto(username=member3, age=30) memberDto = MemberDto(username=member4, age=40)
이 방식의 문제점
- new 오퍼레이션을 써서 DTO의 패키지이름을 다 적어줘야 해서 보기 안좋음.
- 생성자방식만 지원함
⇒ QueryDSL 빈 생성(Bean population)
결과를 DTO로 반환할 때 사용.
다음 3가지 방법 지원
- 프로퍼티 접근
- 필드 직접 접근
- 생성자 사용
// 프로퍼티 접근 - setter로 접근 @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); } } // 필드에 직접 주입 @Test public void findDtoByField() { List<MemberDto> result = queryFactory .select(Projections.fields(MemberDto.class, member.username, member.age)) .from(member) .fetch(); for (MemberDto memberDto : result) { System.out.println("memberDto = " + memberDto); } } // 생성자로 접근 @Test public void findDtoByConstructor() { List<MemberDto> result = queryFactory .select(Projections.constructor(MemberDto.class, member.username, member.age)) .from(member) .fetch(); for (MemberDto memberDto : result) { System.out.println("memberDto = " + memberDto); }
이렇게 하면 똑같이 나온다.
별칭이 다른 경우
package study.querydsl.dto; import lombok.Data; @Data public class UserDto { private String name; private int age; } List<UserDto> fetch = queryFactory .select(Projections.fields(UserDto.class, member.username.as("name"), ExpressionUtils.as( JPAExpressions .select(memberSub.age.max()) .from(memberSub), "age") ) ).from(member) .fetch();
.as(필드에 별칭 적용) 나 ExpressionUtils.as(필드, 서브 쿼리에 적용)로 바꿔서 반환시킬 수 있다.
@QueryProjection 으로 결과 반환
생성자 + @QueryProjection
생성자에 어노테이션을 달고 gradle에서 compileQuerydsl을 실행하면 이 생성자에 대한 것 자체가 Q클래스로 생성된다.
package study.querydsl.dto; import com.querydsl.core.annotations.QueryProjection; import lombok.Data; @Data public class MemberDto { private String username; private int age; public MemberDto() { } @QueryProjection public MemberDto(String username, int age) { this.username = username; this.age = age; } }
@Test public void queryProjection() { List<MemberDto> result = queryFactory .select(new QMemberDto(member.username, member.age)) .from(member) .fetch(); for (MemberDto memberDto : result) { System.out.println("memberDto = " + memberDto); } }
이 방식을 사용하면 컴파일러로 타입을 체크할 수 있어서 QMemberDto의 인자만 잘못 넣어도 빨간줄이 나와서 아주 유용하다.
하지만 DTO에 어노테이션을 유지해야 하는 점과 DTO까지 Q파일을 생성해야 되는 아쉬운 점이 있다.
distinct도 똑같이 .distinct로 사용하면 된다.
드디어, 동적 쿼리
BooleanBuilder 사용
@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(); } 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_.username=? and member0_.age=?
다중 where 파라미터를 사용하는 방법
private List<Member> searchMember2(String usernameCond, Integer ageCond) { return queryFactory .selectFrom(member) .where(usernameEq(usernameCond), ageEq(ageCond)) // 안의 값이 null이면 그냥 무시됨. .fetch(); } private Predicate usernameEq(String usernameCond) { if(usernameCond == null) { return null; } return member.username.eq(usernameCond); } private Predicate ageEq(Integer ageCond) { return ageCond != null ? member.age.eq(ageCond) : null; }
이 방법은 메인 쿼리가 앞에 나오는 두괄식이라고 생각할 수 있다.
메서드 이름만 보고도 바로 뭘 하고싶은지 유추할 수 있도록 만들 수 있기 때문에 BooleanBuilder보다 효율적일 수 있다.
조건을 조립해서 쓰고 싶다면?
BooleanExpression 이라는 추상 클래스가 Predicate를 구현하고 있기 때문에 BooleanExpression으로 조건 메서드를 만들면 조립해서 사용할 수 있다.
private List<Member> searchMember2(String usernameCond, Integer ageCond) { return queryFactory .selectFrom(member) // .where(usernameEq(usernameCond), ageEq(ageCond)) // 안의 값이 null이면 그냥 무시됨. .where(allEq(usernameCond, ageCond)) .fetch(); } private BooleanExpression usernameEq(String usernameCond) { if(usernameCond == null) { return null; } return member.username.eq(usernameCond); } 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)); }
이런 방법을 사용하면 조립해서 다른 쿼리에 넣어서 재활용 할 수도 있다.
수정, 삭제 배치 쿼리
- 쿼리 한 번으로 대량 데이터 수정
@Test public void bulkUpdate() { // DB에 직접 쏴버림 //member1 = 10 -> 비회원 //member2 = 20 -> 비회원 long count = queryFactory .update(member) .set(member.username, "비회원") .where(member.age.lt(28)) .execute(); // 쏴버렸기 때문에 영속성 컨텍스트를 초기화해줘야 됨. em.flush(); em.clear(); List<Member> result = queryFactory .selectFrom(member) .fetch(); for (Member member1 : result) { System.out.println("member1 = " + member1); } }
더하기 빼기는 .add
곱하기는 .multyply
삭제는 .delete
SQL function 호출하기
- Dialect에 들어있는 function들을 가져다 쓰는 방법.
- 보통 ANSI 표준으로 들어있는 함수들은 querydsl에 내장이 되어 있어서 그냥 쓸 수 있다.
- 기본적으로는 Expressions.stringTemplate을 가져다 써야 한다.
실무 활용
순수 JPA Repository와 Querydsl
package study.querydsl.repository; import com.querydsl.jpa.impl.JPAQueryFactory; import org.springframework.stereotype.Repository; import study.querydsl.entity.Member; import javax.persistence.EntityManager; import java.util.List; import java.util.Optional; import static study.querydsl.entity.QMember.member; @Repository public class MemberJpaRepository { private final EntityManager em; private final JPAQueryFactory queryFactory; public MemberJpaRepository(EntityManager em) { this.em = em; this.queryFactory = new JPAQueryFactory(em); } public void save(Member member) { em.persist(member); } public Optional<Member> findById(Long id) { Member findMember = em.find(Member.class, id); return Optional.ofNullable(findMember); } public List<Member> findAll() { return em.createQuery("select m from Member m", Member.class) .getResultList(); } public List<Member> findAll_querydsl() { return queryFactory .selectFrom(member) .fetch(); } public List<Member> findByUsername(String username) { return em.createQuery("select m from Member m where m.username = :username", Member.class) .setParameter("username", username) .getResultList(); } public List<Member> findByUsername_Querydsl(String username) { return queryFactory .selectFrom(member) .where(member.username.eq(username)) .fetch(); } }
JPQL로 작성한 것들과 비교해 보면 가독성 자체가 확실히 좋고 JPQL은 오타를 냈을 때 런타임 에러로만 잡을 수 있지만 querydsl로 작성한 경우에는 컴파일 에러로 미리 잡을 수 있다는 장점이 있다.
** memberJpaRepository에서 new JPAQueryFactory로 queryFactory를 주입받아 사용하게 되어 있는데 스프링 애플리케이션 설정에 @Bean으로 등록하여 사용하는 방법도 있다.
@Bean JPAQueryFactory jpaQueryFactory(EntityManager em) { return new JPAQueryFactory(em); }
이렇게 등록해 놓으면 final로 @ReqArgsConstructor를 사용할 수 있다.
동적 쿼리와 성능 최적화 조회 Builder 사용
MemberTeamDto
package study.querydsl.dto; import com.querydsl.core.annotations.QueryProjection; import lombok.Data; @Data public class MemberTeamDto { private Long memberId; private String username; private int age; private Long teamId; private String teamName; @QueryProjection public MemberTeamDto(Long memberId, String username, int age, Long teamId, String teamName) { this.memberId = memberId; this.username = username; this.age = age; this.teamId = teamId; this.teamName = teamName; } }
MemberSearchCondition
package study.querydsl.dto; import lombok.Data; @Data public class MemberSearchCondition { private String username; private String teamName; private Integer ageGoe; private Integer ageLoe; }
BooleanBuilder를 사용한 동적 쿼리
MemberJpaRepository
public List<MemberTeamDto> searchByBuilder(MemberSearchCondition condition) { BooleanBuilder builder = new BooleanBuilder(); if (StringUtils.hasText(condition.getUsername())) { builder.and(member.username.eq(condition.getUsername())); } if (StringUtils.hasText(condition.getTeamName())) { builder.and(team.name.eq(condition.getTeamName())); } if (condition.getAgeGoe() != null) { builder.and(member.age.goe(condition.getAgeGoe())); } if (condition.getAgeLoe() != null) { builder.and(member.age.loe(condition.getAgeLoe())); } return queryFactory .select(new QMemberTeamDto( member.id, member.username, member.age, team.id, team.name )) .from(member) .leftJoin(member.team, team) .where(builder) .fetch(); }
MemberJpaRepositoryTest
@Test public void searchTest() { 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); MemberSearchCondition condition = new MemberSearchCondition(); condition.setAgeGoe(35); condition.setAgeLoe(40); condition.setTeamName("teamB"); List<MemberTeamDto> result = memberJpaRepository.searchByBuilder(condition); assertThat(result).extracting("username").containsExactly("member4"); } select member0_.member_id as col_0_0_, member0_.username as col_1_0_, member0_.age as col_2_0_, team1_.team_id as col_3_0_, team1_.name as col_4_0_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.team_id where team1_.name=? and member0_.age>=? and member0_.age<=?
where 절에 teamName, age≥, age≤ 가 잘 들어간 채로 조회된 모습이다.
** 조건을 아예 넣지 않았다면 where절이 빠진 채로 전체 조회되는 것에 주의
where절 파라미터 사용을 통한 동적 쿼리
public List<MemberTeamDto> search(MemberSearchCondition condition) { return queryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.username, member.age, team.id.as("teamId"), team.name.as("teamName") )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ) .fetch(); } private BooleanExpression usernameEq(String username) { return hasText(username) ? member.username.eq(username) : null; } private BooleanExpression teamNameEq(String teamName) { return hasText(teamName) ? team.name.eq(teamName) : null; } private BooleanExpression ageGoe(Integer ageGoe) { return ageGoe != null ? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoe(Integer ageLoe) { return ageLoe != null ? member.age.loe(ageLoe) : null; } }
똑같은 코드를 where절 파라미터로 조건을 만들면 조건들을 조합할 수도 있고 재사용도 가능하다.
조회 API 컨트롤러
프로파일 쪼개기
application.yml에 추가하여 실행 프로파일을 구분해줄 수 있다.
spring: profiles: active: local
active 안에 사용할 이름을 넣어주면 된다. dev / prod 이런식으로 많이 쓸 것 같다.
(회사 코드는 아예 resources 폴더 이름 뒤에 suffix로 -dev / -prod를 추가하는 방식으로 사용하긴 한다.)
테스트를 위해 @PostConstruct를 활용하여 테스트용 데이터를 영속성 컨텍스트에 넣어주는 작업을 한다.
initMember 클래스
package study.querydsl; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Profile; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Member; import study.querydsl.entity.Team; import javax.annotation.PostConstruct; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Profile("local") @Component @RequiredArgsConstructor public class initMember { private final InitMemberService initMemberService; @PostConstruct public void init() { initMemberService.init(); } @Component static class InitMemberService { @PersistenceContext private EntityManager em; @Transactional public void init() { Team teamA = new Team("teamA"); Team teamB = new Team("teamB"); em.persist(teamA); em.persist(teamB); for(int i=0; i<100; i++) { Team selectedTeam = i % 2 == 0 ? teamA : teamB; em.persist(new Member("member"+i, i, selectedTeam)); } } } }
MemberController
package study.querydsl.controller; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.repository.MemberJpaRepository; import java.util.List; @RestController @RequiredArgsConstructor public class MemberController { private final MemberJpaRepository memberJpaRepository; @GetMapping("/v1/members") public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) { return memberJpaRepository.search(condition); } }
이렇게 만들어 놓고 애플리케이션을 local 프로파일로 실행하면 처음에 initMember를 통해서 다량의 데이터가 인서트 된다.
/v1/members에 쿼리 파라미터로 조건을 넘기면 데이터를 잘 받아옴을 확인할 수 있다.
이런식으로 요청하면
[ { "memberId": 34, "username": "member31", "age": 31, "teamId": 2, "teamName": "teamB" }, { "memberId": 36, "username": "member33", "age": 33, "teamId": 2, "teamName": "teamB" }, ... ... ... ... { "memberId": 102, "username": "member99", "age": 99, "teamId": 2, "teamName": "teamB" } ]
위와 같은 형태로 응답이 오고
select member0_.member_id as col_0_0_, member0_.username as col_1_0_, member0_.age as col_2_0_, team1_.team_id as col_3_0_, team1_.name as col_4_0_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.team_id where team1_.name=? and member0_.age>=?
쿼리도 잘 나가는 것을 확인할 수 있다.
실무 활용 - 스프링 데이터 JPA와 Querydsl
스프링 데이터 JPA Repository로 변경
memberRepository 인터페이스 생성
package study.querydsl.repository; import org.springframework.data.jpa.repository.JpaRepository; import study.querydsl.entity.Member; import java.util.List; public interface memberRepository extends JpaRepository<Member, Long> { List<Member> findByUsername(String username); }
MemberRepositoryTest를 만들어서 MemberJPArepository와 똑같은 테스트를 돌려본다.
@ActiveProfiles 어노테이션을 붙이지 않으면 test 패키지 안에 application.yml에 test 프로파일 설정을 만들어 놨음에도 local로 실행되버리는 문제가 있었다.
package study.querydsl.repository; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import study.querydsl.entity.Member; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @SpringBootTest @Transactional @ActiveProfiles("test") class MemberRepositoryTest { @PersistenceContext EntityManager em; @Autowired MemberRepository memberRepository; @Test public void basicTest() { Member member = new Member("member1", 10); memberRepository.save(member); Member findMember = memberRepository.findById(member.getId()).get(); assertThat(findMember).isEqualTo(member); List<Member> result1 = memberRepository.findAll(); assertThat(result1).containsExactly(member); List<Member> result2 = memberRepository.findByUsername("member1"); assertThat(result2).containsExactly(member); } }
설정파일은 위의 링크 대로 설정하는 것이 정석이라고 생각된다.
일단은 공부용이니까 @ActiveProfiles로 처리하기로 했다.
사용자 정의 레포지토리
MemberRepositoryCustom 인터페이스를 만들고 MemberRepository에서 상속받는다.
querydsl을 활용한 커스텀 쿼리는 MemberRepositoryImpl 클래스를 만들어서 MemberRepositoryCustom을 구현한다.
package study.querydsl.repository; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import java.util.List; public interface MemberRepositoryCustom { List<MemberTeamDto> search(MemberSearchCondition condition); }
package study.querydsl.repository; import org.springframework.data.jpa.repository.JpaRepository; import study.querydsl.entity.Member; import java.util.List; public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom { List<Member> findByUsername(String username); }
package study.querydsl.repository; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.dto.QMemberTeamDto; import java.util.List; import static org.springframework.util.StringUtils.hasText; import static study.querydsl.entity.QMember.member; import static study.querydsl.entity.QTeam.team; public class MemberRepositoryImpl implements MemberRepositoryCustom { private final JPAQueryFactory queryFactory; public MemberRepositoryImpl(JPAQueryFactory queryFactory) { this.queryFactory = queryFactory; } @Override public List<MemberTeamDto> search(MemberSearchCondition condition) { return queryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.username, member.age, team.id.as("teamId"), team.name.as("teamName") )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ) .fetch(); } private BooleanExpression usernameEq(String username) { return hasText(username) ? member.username.eq(username) : null; } private BooleanExpression teamNameEq(String teamName) { return hasText(teamName) ? team.name.eq(teamName) : null; } private BooleanExpression ageGoe(Integer ageGoe) { return ageGoe != null ? member.age.goe(ageGoe) : null; } private BooleanExpression ageLoe(Integer ageLoe) { return ageLoe != null ? member.age.loe(ageLoe) : null; } }
만약 특정 api에 사용되는 쿼리를 위한 복잡한 코드를 만들어서 쓴다고 하면 MemberQueryRepository 등의 클래스를 만들어서 @Repository로 설정해주고 이 클래스를 주입받아서 쓰는 것도 좋은 방법이라고 할 수 있다.
복잡한 코드들을 전부 다 Custom에 넣어버리는 것도 좋은 방법은 아니기 때문이다.
스프링 데이터 페이징 활용
- 스프링 데이터의 Page, Pageable을 활용
- 전체 카운트를 한번에 조회하는 단순한 방법
- 데이터 내용과 전체 카운트를 별도로 조회하는 방법
querydsl 5.0.0 이후부터는 fetchResults()가 deprecated 되었다는 것에 유의.
public interface MemberRepositoryCustom { List<MemberTeamDto> search(MemberSearchCondition condition); Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable); Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable); }
MemberRepositoryCustom에 페이징 관련 메서드를 추가.
@Override public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) { List<MemberTeamDto> content = queryFactory .select(new QMemberTeamDto( member.id.as("memberId"), member.username, member.age, team.id.as("teamId"), team.name.as("teamName") )) .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(new QMemberTeamDto( member.id.as("memberId"), member.username, member.age, team.id.as("teamId"), team.name.as("teamName") )) .from(member) .leftJoin(member.team, team) .where( usernameEq(condition.getUsername()), teamNameEq(condition.getTeamName()), ageGoe(condition.getAgeGoe()), ageLoe(condition.getAgeLoe()) ) .fetch().size(); return new PageImpl<>(content, pageable, total); }
offset과 limit을 pageable에서 꺼내서 넣으면 된다.
fetch().size()로 기존의 fetchCount()를 대체할 수 있다.
PageableExecutionutils.getPage()를 사용하는 방법은 FetchCount가 deprecated 되면서 사용할 수 없는 상태이다.
컨트롤러 활용 페이징 처리
일단 default profile 이름이 default로 들어가 있어서 애플리케이션 실행시 initMember가 실행되지 않아서 application.yml을 변경해주었다. ActiveProfiles 어노테이션은 test클래스에서만 활용이 가능한 spring-boot-test 의존성에 포함된 기능이었다.
spring: datasource: url: jdbc:h2:tcp://localhost/~/querydsl username: sa password: driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create properties: hibernate: format_sql: true profiles: default: local # show_sql: true logging: level: org.hibernate.SQL: debug
MemberController에 v2/members를 추가해주었다.
package study.querydsl.controller; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import study.querydsl.dto.MemberSearchCondition; import study.querydsl.dto.MemberTeamDto; import study.querydsl.repository.MemberJpaRepository; import study.querydsl.repository.MemberRepository; import java.util.List; @RestController @RequiredArgsConstructor public class MemberController { private final MemberJpaRepository memberJpaRepository; private final MemberRepository memberRepository; @GetMapping("/v1/members") public List<MemberTeamDto> searchMemberV1(MemberSearchCondition condition) { return memberJpaRepository.search(condition); } @GetMapping("/v2/members") public Page<MemberTeamDto> searchMemberV2(MemberSearchCondition condition, Pageable pageable) { return memberRepository.searchPageSimple(condition, pageable); } }
{ "content": [ { "memberId": 3, "username": "member0", "age": 0, "teamId": 1, "teamName": "teamA" }, { "memberId": 4, "username": "member1", "age": 1, "teamId": 2, "teamName": "teamB" }, { "memberId": 5, "username": "member2", "age": 2, "teamId": 1, "teamName": "teamA" }, { "memberId": 6, "username": "member3", "age": 3, "teamId": 2, "teamName": "teamB" }, { "memberId": 7, "username": "member4", "age": 4, "teamId": 1, "teamName": "teamA" } ], "pageable": { "sort": { "unsorted": true, "sorted": false, "empty": true }, "pageNumber": 0, "pageSize": 5, "offset": 0, "paged": true, "unpaged": false }, "totalPages": 20, "totalElements": 100, "last": false, "numberOfElements": 5, "sort": { "unsorted": true, "sorted": false, "empty": true }, "number": 0, "size": 5, "first": true, "empty": false }
totalElements자리에 count쿼리로 따로 뽑은 전체 데이터 개수가 들어간다.
스프링 데이터 정렬(Sort)과 querydsl
스프링 데이터 Sort는 조건이 복잡해지면 제대로 동작하지 않는다. 따라서 Sort 사용보다는 파라미터를 받아서 직접 처리해야 한다.
실제 쿼리 dsl로 작업한 내용들 (버리기 아까워…)
package com.wefunding.seoul.exchange.trade.domain; import static com.wefunding.seoul.common.domain.contract.QContract.contract; import static com.wefunding.seoul.exchange.trade.domain.QOrders.orders; import static com.wefunding.seoul.exchange.trade.domain.QTrades.trades; import com.querydsl.core.BooleanBuilder; import com.querydsl.core.QueryResults; import com.querydsl.core.types.Expression; import com.querydsl.core.types.dsl.CaseBuilder; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.core.types.dsl.NumberExpression; import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; import com.wefunding.seoul.common.config.CustomPage; import com.wefunding.seoul.common.domain.enums.InvestmentType; import com.wefunding.seoul.exchange.exchange_common.domain.enums.trade.OrderStatus; import com.wefunding.seoul.exchange.exchange_common.domain.enums.trade.OrderType; import com.wefunding.seoul.exchange.trade.application.data.*; import java.time.LocalDate; import java.time.LocalTime; import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.PageRequest; import org.springframework.util.StringUtils; @RequiredArgsConstructor @Slf4j public class TradesRepositoryImpl implements TradesRepositoryCustom { private final JPAQueryFactory jpaQueryFactory; @Override public CustomPage findAllTradesForGetTransactionList( long userInfoId, long contractId, OrderStatus orderStatus, PageRequest pageRequest) { NumberExpression<Long> total = trades.executedPrice.multiply(trades.executedQuantity); QueryResults<TransactionResponseVO> queryResults = jpaQueryFactory .select( new QTransactionResponseVO( trades.createdAt, trades.orders.orderType, trades.executedPrice, total, trades.executedQuantity)) .from(trades) .where( trades.orders.userInfoId.eq(userInfoId), trades.orders.contractId.eq(contractId), trades.orders.orderStatus.in(OrderStatus.FINISH, OrderStatus.PARTIAL)) .orderBy(trades.createdAt.desc()) .offset(pageRequest.getOffset()) .limit(pageRequest.getPageSize()) .fetchResults(); return new CustomPage<>(queryResults.getResults(), pageRequest, queryResults.getTotal()); } @Override public CustomPage<HoldingAssetsVO> findAllHoldingsForMyPage( long userInfoId, PageRequest pageRequest) { QueryResults<HoldingAssetsVO> queryResults = jpaQueryFactory .select( new QHoldingAssetsVO( trades.orders.contractId, Expressions.asString("계약명" + orders.contractId), quantityTotal, executedPriceTotal, Expressions.constant(1100L))) // TODO: orderbook에서 나중에 시세 가져와야 함. .from(trades) .innerJoin(trades.orders, orders) .where(trades.orders.userInfoId.eq(userInfoId)) .groupBy(orders.contractId) .offset(pageRequest.getOffset()) .limit(pageRequest.getPageSize()) .fetchResults(); return new CustomPage<>(queryResults.getResults(), pageRequest, queryResults.getTotal()); } @Override public List<HoldingAssetsVO> getDashboardInfo(long userInfoId) { return jpaQueryFactory .select( new QHoldingAssetsVO( trades.orders.contractId, Expressions.asString("계약명" + orders.contractId), quantityTotal, executedPriceTotal, Expressions.asNumber( quantityTotal.multiply(1100L)))) // TODO: orderbook에서 나중에 시세 가져와야 함. .from(trades) .innerJoin(trades.orders, orders) .where(trades.orders.userInfoId.eq(userInfoId)) .groupBy(orders.contractId) .fetchResults() .getResults(); } @Override public CustomPage getMypageTradesHistory( long userInfoId, OrderStatus orderStatus, LocalDate startDate, LocalDate endDate, OrderType orderType, String contractName, PageRequest pageRequest) { BooleanBuilder condition = mypageTradesHistoryCondition(orderStatus, orderType, contractName, startDate, endDate); NumberExpression<Double> fee = new CaseBuilder() .when(contract.type.eq(InvestmentType.PF)) .then(0.3) .when(contract.type.eq(InvestmentType.REAL)) .then(0.25) .otherwise(0.0) .doubleValue(); Expression<String> contractNameSubQuery = JPAExpressions.select(contract.name) .from(contract) .where(contract.contractId.eq(trades.orders.contractId)); QueryResults<MypageExecutedTradesHistoryVO> queryResults = jpaQueryFactory .select( new QMypageExecutedTradesHistoryVO( trades.createdAt, contractNameSubQuery, orders.orderType, trades.executedQuantity, trades.executedPrice, fee, trades.executedQuantity.multiply(trades.executedPrice), orders.createdAt)) .from(trades) .innerJoin(trades.orders, orders) .join(contract) .on(contract.contractId.eq(trades.orders.contractId)) .where(condition) .where(trades.orders.userInfoId.eq(userInfoId)) .orderBy(trades.createdAt.desc()) .offset(pageRequest.getOffset()) .limit(pageRequest.getPageSize()) .fetchResults(); return new CustomPage<>(queryResults.getResults(), pageRequest, queryResults.getTotal()); } private final NumberExpression<Long> quantityTotal = new CaseBuilder() .when(orders.orderType.eq(OrderType.BUY)) .then(trades.executedQuantity) .otherwise(trades.executedQuantity.negate()) .sum(); private final NumberExpression<Long> executedPriceTotal = new CaseBuilder() .when(orders.orderType.eq(OrderType.BUY)) .then(trades.executedPrice.multiply(trades.executedQuantity)) .otherwise(trades.executedPrice.multiply(trades.executedQuantity).negate()) .sum(); private BooleanBuilder mypageTradesHistoryCondition( OrderStatus orderStatus, OrderType orderType, String keyword, LocalDate startDate, LocalDate endDate) { BooleanBuilder allCond = new BooleanBuilder(); if (orderStatus != OrderStatus.WAIT) { allCond.and(trades.orders.orderStatus.in(OrderStatus.FINISH, OrderStatus.PARTIAL)); } if (orderType != OrderType.ALL) { allCond.and(orders.orderType.eq(orderType)); } if (StringUtils.hasText(keyword)) { allCond.and(contract.name.contains(keyword)); } if (startDate != null) { allCond.and(trades.createdAt.goe(startDate.atStartOfDay())); } if (endDate != null) { allCond.and(trades.createdAt.loe(endDate.atTime(LocalTime.MAX))); } return allCond; } }