티스토리 뷰
아래 내용은 QueryDSL - 김영한 강의 영상을 보고 실무에서 필요할 때
찾아보려고 정리한 내용이며 QueryDSL 문법에 순서를 적용하여 찾아보기 쉽게 작성했습니다.
그리고 아래 문법을 테스트할 때 사용했던 모든 버전은 QueryDSL 설정 글에서 확인하시면 됩니다
아래로 조금만 내리시면 작성된 문법들에 대한 표가 있고
필요한 문법 이름을 복사한 후 문자열 찾기 하시면 해당 내용으로 빠르게 가실 수 있습니다
사전 준비
Q엔티티 static import, JPAQueryFactory 생성
import static com.jpa.jpaboilerplate.entity.QMember.member;
import static com.jpa.jpaboilerplate.entity.QTeam.team;
JPAQueryFactory queryFactory = new JPAQueryFactory(em);
테스트용 Member, Team 엔티티 설정
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
// 연관 관계 설정된 필드 넣지 말 것
@ToString(of = {"id", "username", "age"})
public class Member extends BaseEntity {
@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);
}
}
//== 연관 관계 편의 메서드 ==//
public 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
@Column(name = "team_id")
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
public Team(String name) {
this.name = name;
}
}
1. 기본 select, from, selectFrom |
2. 별칭 as |
3. 비교 eq (==), eq.not (!=) |
4. 조건절 and, or |
5. 결과 fetch, fetchOne, fetchFirst and Deprecated methods |
6. Deprecated fetchCount 해결 방법 |
7. 정렬 desc, asc |
8. Tuple (QueryDSL 반환 타입) |
9. 집계 count, sum, avg, max, min |
10. groupBy, having |
11. 조인, 세타 조인(막무가내 조인) |
12. 조인 on 절 |
13. join fetch |
14. sub query |
15. case, CaseBuilder(case 내용이 복잡한 경우) |
16. 데이터에 상수 추가 |
17. concat (문자열 더하기) |
18. 프로젝션(select 반환 대상), 대상 1~2개, DTO 반환, @QueryProjection |
19. 동적 쿼리 |
20. 수정, 삭제 벌크 |
21. SQL Function |
1. 기본 select, from, selectFrom
@Test
void Querydsl() {
Member findMember = queryFactory
.select(member)
.from(member)
// 조회 대상이 엔티티 하나인 경우 .selectFrom() 사용 가능
//.selectFrom(member)
.fetchOne();
}
2. 별칭 as
만약 같은 엔티티에 대해서 조인을 진행해야 될 경우
QMember subMember = new QMember("subMember");
와 같이 새로운 별칭을 가진 멤버를 생성하고 조인해야됩니다
@Test
void as() {
List<String> memberNames = queryFactory
.select(member.username.as("memberName"))
.from(member)
.fetch();
}
3. 비교 eq (==), eq.not (!=)
@Test
void eqAndEqNot() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("usernameA"))
.fetchOne();
List<Member> findUsers = queryFactory
.selectFrom(member)
.where(member.username.eq("usernameA").not())
.fetch();
}
4. 조건절 and, or
콤마(,) 를 사용한 방법은 and 조건과 같습니다
@Test
void and_or() {
List<Member> findMembers = queryFactory
.selectFrom(member)
.where(
member.username.eq("usernameA"),
member.age.eq(20)
)
//.where(member.username.eq("usernameA").and(member.age.eq(20)))
//.where(member.username.eq("usernameA").or(member.age.eq(20)))
.fetch();
}
5. 결과 fetch, fetchOne, fetchFirst and Deprecated methods
- Deprecated methods
- GroupBy, Having절을 사용할 경우 이슈가 많다고 합니다
- fetchCount: 토탈 카운트
- fetchResult: 데이터 + 토탈 카운트
@Test
void result() {
queryFactory
.selectFrom(member)
.where(member.age.goe(10)) // goe 설명은 따로 있습니다
.fetch(); // List Data
//.fetchOne(); // Single Data
//.fetchCount(); // deprecated
//.fetchResults(); // deprecated
//.fetchFirst(); // limit(1) + fetchOne() 과 같습니다
}
6. Deprecated fetchCount 해결 방법
@Test
void totalCount() {
Long totalCount = queryFactory
.select(member.count())
.from(member)
.fetchOne();
}
7. 정렬 desc, asc
아래에 적용한 정렬 조건은 아래와 같습니다
회원 나이 내림 차순, 회원 이름 올림 차순, 회원 이름이 없으면 마지막에 출력
@Test
void sort() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
}
8. Tuple (QueryDSL 반환 타입)
Tuple 클래스는 여러 엔티티, 값 또는 여러 타입들이 포함될 경우 반환하기 위해 사용합니다
아래는 List<Tuple> 의 필드를 사용하는 방법입니
@Test
void tuple() {
List<Tuple> tuples = queryFactory
.select(member.username, team.name)
.from(member)
.join(member.team, team)
.fetch();
for (Tuple tuple : tuples) {
String username = tuple.get(member.username);
String teamName = tuple.get(team.name);
}
}
9. 집계 count, sum, avg, max, min
@Test
void aggregation() {
Tuple tuple = queryFactory
.select(
member.count(),
member.age.sum(),
member.age.avg(),
member.age.max(),
member.age.min())
.from(member)
.fetchOne();
Long count = tuple.get(member.count());
Integer num = tuple.get(member.age.sum());
Double avg = tuple.get(member.age.avg());
Integer max = tuple.get(member.age.max());
Integer min = tuple.get(member.age.min());
}
10. groupBy, having
@Test
void groupAndHaving() {
List<Tuple> tuples = queryFactory
.select(team.name, member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name).having(member.age.avg().gt(10)) // 나이 평균이 10보다 큼
.fetch();
for (Tuple tuple : tuples) {
String teamName = tuple.get(team.name);
Double ageAvg = tuple.get(member.age.avg());
}
}
11. 조인, 세타 조인
세타 조인은 막무가내로 조인하는 느낌이 강하며 아래 예시는
사용자 이름과 팀 이름이 같은 경우 조인을 하는 예시입니다
@Test
void join() {
// 일반 조인
List<Member> members = queryFactory
.selectFrom(member)
.join(member.team, team) // .join() == .innerJoin()
//.leftJoin()
//.rightJoin()
.fetch();
}
@Test
void thetaJoin() {
// 세타 조인
List<Member> result = queryFactory
.select(member)
.from(member, team)
.where(member.username.eq(team.name))
.fetch();
}
12. 조인 on 절
@Test
void on() throws Exception {
List<Tuple> tuples = queryFactory
.select(member, team)
.from(member)
.leftJoin(team).on(member.username.eq(team.name))
.where(team.name.eq("teamA"))
.fetch();
}
13. join fetch
우선 member에서 team은 FetchType.LAZY 로 설정되어 있습니다
EntityManagerFactory 를 호출하는 이유는 .fetchJoin() 을 적용했을 때 Member, Team 을 조인해서
데이터를 쿼리 한 번에 가져오는지 그리고 영속성 컨텍스트에서 관리중인지 확인하기 위함입니다
@PersistenceUnit
EntityManagerFactory emf;
@Test
void join() {
Member findMember = queryFactory
.selectFrom(member)
.where(member.username.eq("usernameA"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).isFalse();
}
@Test
void fetchJoin() throws Exception {
Member findMember = queryFactory
.selectFrom(member)
.join(member.team, team).fetchJoin()
.where(member.username.eq("usernameA"))
.fetchOne();
boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());
assertThat(loaded).isTrue();
}
14. sub query
공부한 시점에 from 절에 서브쿼리는 JPA, QueryDSL 둘 다 지원하지 않았습니다
혹시나 가능하도록 업데이트 되었다면 해당 정보를 댓글로 남겨주시면 감사하겠습니다
그리고 지원하지 않은 시점에서 from 절에 sub query 가 필요할 정도로 복잡해진다면
nativeSQL 사용하여 해결할 수 있으니 검색해보시면 좋을 것 같습니
@Test
void subQuery() {
//when
QMember memberSub = new QMember("memberSub");
List<Member> findMembers = queryFactory
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max()) // max age
.from(memberSub)
))
.fetch();
//then
assertThat(findMembers)
.extracting("age")
.containsExactly(40);
}
15. case, CaseBuilder(case 내용이 복잡한 경우)
case 절을 성능에 대한 관점으로 바라볼 때 쿼리에서 적용하여 분류하는 것 보다
어플리케이션 레벨에서 처리하는게 더 좋다고 합니다 :)
@Test
void case() {
List<String> result = queryFactory
.select(member.age
.when(10).then("열살")
.when(20).then("스무살")
.otherwise("기타"))
.from(member)
.fetch();
}
@Test
void complexCase() throws Exception {
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();
}
16. 데이터에 상수 추가
아래 로직을 보시면 상수를 포함한 조회를 진행하는 것 처럼 보이나
실제 전달한 쿼리 로그를 확인해 보시면 상수에 대한 내용은 없습니다
데이터를 받아온 후 상수 값을 넣는 방식을 사용합니다
@Test
void constant() {
List<Tuple> result = queryFactory
.select(member, Expressions.constant("A"))
.from(member)
.fetch();
}
17. concat (문자열 더하기)
@Test
void concat() {
//when
List<String> result = queryFactory
.select(member.username.concat("_").concat(member.age.stringValue()))
.from(member)
.fetch();
}
18. 프로젝션(select 반환 대상), 대상 1~2개, DTO 반환, @QueryProjection
18_1. 반환 대상이 1~2개
18_2. 프로젝션을 DTO 로 반환하는 세 가지 방법
18_3. 프로젝션을 DTO 로 반환의 단점 극복, @QueryProjection 사용
18_1. 반환 대상이 1~2개
@Test
void projection() {
// 반환 대상 1개(username)
List<String> targetOne = queryFactory
.select(member.username).from(member).fetch();
// 반환 대상 2개(username, age)
List<Tuple> targetTwo = queryFactory
.select(member.username, member.age).from(member).fetch();
}
18_2. 프로젝션을 DTO 로 반환하는 세 가지 방법
Projections.bean, Projections.fields, Projections.constructor
Projections.constuctor 를 제외한 나머지는 필드명이 다를 경우 null 값이 주입됩니다
그리고 필드명이 다를 때 별칭을 아래와 같이 적용하여 Dto 와 맞춰주면 문제를 해결할 수 있습니다
아래 방법들의 큰 단점은 안에 잘못된 필드를 마음대로 추가해도 컴파일 레벨에서 확인하지 못합니다
해당 문제는 런타임 레벨에서 확인되며 즉 사용자에 의해 발견됩니다
@Test
void projectionToDto() {
List<UserDto> users = queryFactory
//.select(Projections.bean(UserDto.class, member.username, member.age)) // 필드명이 달라서 이름이 null
//.select(Projections.bean(UserDto.class, member.username.as("name"), member.age)) 필드명 다를 때 as 사용하는 방법
//.select(Projections.fields(UserDto.class, member.username, member.age)) // fields 또한 필드명이 달라서 이름이 null
//.select(Projections.fields(UserDto.class, member.username.as("name"), member.age))
//.select(Projections.constructor(UserDto.class, member.username, member.age)) // 타입만 같으면 결과가 정상적으로 출력
.select(Projections.fields(UserDto.class, // 서브 쿼리의 결과에 별칭 사용하는 방법
member.username.as("name"),
Expressions.as(JPAExpressions
.select(member.age.max())
.from(member), "age")
))
.from(member)
.fetch();
}
18_3. 프로젝션을 DTO 로 반환의 단점 극복, @QueryProjection 사용
@QueryProjection 적용 후 Maven, Gradle 에서 (Gradle 기준)Tasks - compileJava 실행하여
Q 로 시작하는 Dto 가 생성되어야 사용 가능합니다
아래와 같이 적용하면 생성자에 잘못된 필드가 들어갈 때 즉 컴파일 단계에서 에러를 잡을 수 있습니다
import com.querydsl.core.annotations.QueryProjection;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
@Test
void queryProjectionAnnotation() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
}
19. 동적 쿼리
동적 쿼리를 적용하는 방법은 아래와 같이 심플하게 하나의 메서드에서 모든 조건을 만들고
처리하는 방법과 조건이 될 수 있는 여러사항을 모두 구분하여 필요한 조합으로 묶어
적용하는 2가지 방법이 있습니다
19_1. 조건을 하나의 메서드에 직접 명시해서 해결하는 방법
19_2. 조건을 BooleanExpression 로 분리하여 묶어서 해결하는 방법(실무에서 사용 권장)
19_1. 조건을 하나의 메서드에 직접 명시해서 해결하는 방법
@Test
void booleanBuilder() {
String username = "usernameA";
int age = 10;
List<Member> members = searchMembers(username, age);
for (Member m : members) {
System.out.println("m = " + m);
}
}
private List<Member> searchMembers(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();
}
19_2. 조건을 BooleanExpression 로 분리하여 묶어서 해결하는 방법(실무에서 사용 권장)
@Test
void dynamicQuery() {
String username = "usernameA";
int age = 10;
List<Member> members = queryFactory
.selectFrom(member)
// predicate 사용하여 각 각의 메서드 표현
//.where(usernameEq(username), ageEq(age))
// BooleanExpression 사용하여 조합해서 한 번에 표현
.where(AllEq(username, age))
.fetch();
for (Member member : members) {
System.out.println("member = " + member);
}
}
private Predicate usernameEq(String username) {
return username != null ? member.username.eq(username) : null;
}
private Predicate ageEq(Integer age) {
return age != null ? member.age.eq(age) : null;
}
// 메서드를 조합해서 사용하는 방법
private BooleanExpression AllEq(String username, Integer age) {
return usernameEq2(username).and(ageEq2(age));
}
private BooleanExpression usernameEq2(String username) {
return username != null ? member.username.eq(username) : null;
}
private BooleanExpression ageEq2(Integer age) {
return age != null ? member.age.eq(age) : null;
}
20. 수정, 삭제 벌크
벌크 연산은 영속성 컨텍스트에 영향을 주지 않습니다
벌크 연산 이 후 비즈니스 로직이 필요한 경우는 영속성 컨텍스트를 모두 비우고
필요한 엔티티를 다시 조회한 후 비즈니스 로직을 적용해야 됩니다
벌크 연산 이 후 비즈니스 로직을 가지지 않은 방법을 권장합니다
@Test
void bulk() {
long 수정_영향_받은_행_수 = queryFactory
.update(member)
//.set(member.username, "비회원") // 문자열 변경
.set(member.age, member.age.add(1)) // 숫자, -숫자 변경
.where(member.age.lt(28))
.execute();
long 삭제_영향_받은_행_수 = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
em.flush();
em.clear();
}
21. SQL Function
DB에서 제공하는 함수를 사용하는 방법
ANSI SQL Function(모든 DB에서 기본적으로 제공하는 function)은 QueryDSL에서도 지원
추가적인 기능이 필요한 경우 'DB명' + Dilect 상속 받고 커스텀 해야합니다
@Test
void sqlFunction() {
List<String> result = queryFactory
.select(Expressions.stringTemplate(
"function('lower', {0})", member.username)) // where 절에서도 사용 가능
//.select(member.username.lower()) // QueryDSL 에서 지원하는 함수
.from(member)
.fetch();
}
'JPA > QueryDSL' 카테고리의 다른 글
QueryDSL 설정, QueryRepositorySupport & Paging, WEB 지원 (0) | 2023.11.13 |
---|
- Total
- Today
- Yesterday
- Spring boot
- letsencrypt
- querydsl
- AWS Opensearch
- codedeploy
- 도메인 내부 테스트
- REST API
- CodeBuild
- JPA 벌크성 수정 쿼리
- CodePipeline
- AWS 로드밸런서 SSL 등록
- AWS CodePipeline
- aws codebuild
- properties 암호화
- jasypt
- logstash
- ELK
- Spring Boot 3.x
- 네임 서버 변경
- QueryDSL 사용 방법
- 시스템 환경변수
- aws codecommit
- Certbot
- aws codedeploy
- certonly
- AWS MSK
- Spring Data JPA
- AWS 자동 배포
- 후이즈에서 AWS Route 53
- ssl nginx 라우팅
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |