- querydsl 5.0
- java 17
- springboot 3.2.5
build.gradle 설정
complile querydsl
Gradle tab → Tasks → other → compileJava (compileQuerydsl)
Q Class 생성 위치
build/generated/sources/annotationProcessor/*
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.5'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'jpabook'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// Querydsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}
tasks.named('test') {
useJUnitPlatform()
}
clean {
delete file('src/main/generated')
}
기본 문법
기본 예제
jpql: "select m from Member m where m.name = :name and m.age = :age" 과 동일
@Repository
@RequiredArgsConstructor
public class MemberQuerydslRepository {
private final EntityManager em;
public Member findByNameAndAge(String name, int age) {
JPAQueryFactory query = new JPAQueryFactory(em);
return query
.selectFrom(member)
.where(
member.name.eq(name),
member.age.eq(age))
.fetchOne();
}
}
검색 조건
member.username.eq("hui") // username = 'hui'
member.username.ne("hui") // username != 'hui'
member.username.eq("hui").not() // username != 'hui'
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("hui%") // like 검색
member.username.contains("hui%") // like '%hui%' 검색
member.username.startsWith("hui%") // like 'hui%' 검색
결과 조회
- fetch(): list 조회, 데이터 없으면 빈 list 반환
- fetchOne(): 단 건 조회
- 결과가 없으면 null
- 결과가 둘 이상일 경우, NonUniqueResultException 예외 발생
- fetchFirst(): limit(1). fetchOne()
fetchResults(): 페이징 정보 포함, total count 쿼리 추가 실행→ @DeprecatedfetchCount(): count 쿼리로 변경, count 조회→ @Deprecated
// List
List<Member> members = query
.selectFrom(member)
.fetch();
// 단 건
Member findMember = query
.selectFrom(member)
.fetchOne();
// 처음 한 건 조회
Member findMember = query
.selectFrom(member)
.fetchFirst();
// 페이징에서 사용
QueryResults<Member> results = query
.selectFrom(member)
.fetchResults();
// count 쿼리로 변경
long count = query
.selectFrom(member)
.fetchCount();
Querydsl의 fetchCount(), fetchResult()는 단순한 쿼리는 문제가 없었으나, 복잡한 쿼리일 경우 정상적으로 작동되지 않아 향후 지원하지 않게 되었습니다. 따라서 count 쿼리가 필요하다면 다음과 같이 별도로 작성해야합니다.
Long totalCount = query
// .select(Wildcard.count) // select count(*)
.select(member.count()) // select count(member.id)
.from(member)
.fetchOne();
페이징
페이징 쿼리의 경우 select 쿼리와 별도로 count 쿼리를 작성하여 fetch()를 사용해야 합니다.
import org.springframework.data.support.PageableExecutionUtils; // 패키지 변경
public Page<MemberTeamDto> searchPage(MemberSearchCondition condition, Pageable pageable) {
List<MemberTeamDto> content = query
.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();
JPAQuery<Long> countQuery = query
.select(member.count())
.from(member)
.leftJoin(member.team, team)
.where(
usernameEq(condition.getUsername()),
teamNameEq(condition.getTeamName()),
ageGoe(condition.getAgeGoe()),
ageLoe(condition.getAgeLoe())
);
// 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때 또는 마지막 페이지 일 때
// offset + 컨텐츠 사이즈를 더해서 전체 사이즈 구함, 마지막 페이지이면서 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
// 쿼리를 생성하지 않음!!
return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
}
정렬
- desc() , asc(): 일반 정렬
- nullsLast() , nullsFirst(): null 데이터 순서 부여
/**
* 1. 회원 나이 내림차순(desc)
* 2. 회원 이름 올림차순(asc)
* 3. 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
*/
List<Member> members = query
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
집합
// tuple: 셀수있는 수량의 순서있는 열거 또는 어떤 순서를 따르는 요소들의 모음
List<Tuple> result = query
.select(
member.count(), // 회원 수
member.age.sum(), // 나이 합
member.age.avg(), // 나이 평균
member.age.max(), // 최대 나이
member.age.min()) // 최소 나이
.from(member)
.fetch();
groupBy & having 예시
List<Tuple> result = query
.select(
team.name,
member.age.avg())
.from(member)
.join(member.team, team)
.groupBy(team.name) // teamName으로 groupBy
.having(member.age.avg().gt(20)) // 회원 평균 나이 20세 이상만 조회
.fetch();
조인
기본 조인 및 on 절
- member와 team을 join 및 on절 활용(join team의 name이 "teamA"만 가져온다.)
List<Member> members = query
.selectFrom(member)
.join(member.team, team).on(team.name.eq("teamA"))
.where(team.name.eq("teamA"))
.fetch();
세타(theta) 조인
- member name이 team name과 같은 member 조회(외부 조인 불가능)
List<Member> members = query
.select(member)
.from(member, team) // 서로 연관관계가 없이 조인 가능 (from절 나열하는 것과 동일)
.where(member.name.eq(team.name))
.fetch();
연관 관계가 없는 외부 조인
- member name과 team name이 같은 대상 외부 조인
List<Tuple> result = query
.select(member, team)
.from(member)
.leftJoin(team).on(member.name.eq(team.name)) // team -> id 매칭 X, member.name, team.name 만 매칭
// .leftJoin(member.team, team).on(member.name.eq(team.name)) // member.team, team -> id 매칭
.fetch();
페치 조인
Member findMember = query
.selectFrom(member)
.join(member.team, team).fetchJoin() // fetch join 사용
.where(member.name.eq("hui"))
.fetchOne();
서브 쿼리
QMember memberSub = new QMember("memberSub");
// 나이가 가장 많은 회원 조회
List<Member> maxMembers = query
.selectFrom(member)
.where(member.age.eq(
JPAExpressions
.select(memberSub.age.max())
.from(memberSub)))
.fetch();
// 나이가 평균 이상인 회원
List<Member> avgMembers = query
.selectFrom(member)
.where(member.age.goe(
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub)))
.fetch();
// 15살 초과 회원들 in절
List<Member> members = query
.selectFrom(member)
.where(member.age.in(
JPAExpressions
.select(memberSub.age)
.from(memberSub)
.where(member.age.gt(15))))
.fetch();
// select절에 subquery
List<Tuple> result = query
.select(
member.name,
JPAExpressions
.select(memberSub.age.avg())
.from(memberSub))
.from(member)
.fetch();
Case 문
List<Tuple> results = query
.select(member.name,
member.age
.when(10).then("10살")
.when(20).then("20살")
.otherwise("기타"))
.from(member)
.fetch();
List<Tuple> results = query
.select(member.name,
new CaseBuilder()
.when(member.age.between(0, 10)).then("0 ~ 10살")
.when(member.age.between(11, 20)).then("11 ~ 20살")
.when(member.age.between(21, 30)).then("21 ~ 30살")
.otherwise("기타"))
.from(member)
.fetch();
상수, 문자 더하기
// ex) [{member.name}, "A"], ...
List<Tuple> result = query
.select(member.name, Expressions.constant("A"))
.from(member)
.fetch();
// ex) {member.name} : {age} -> hui : 10...
StringExpression memberAge = member.age.stringValue(); // 정수를 문자열로 변환
List<String> results = query
.select(member.name.concat(" : ").concat(memberAge))
.from(member)
.fetch();
중급 문법
프로젝션 DTO 조회
- Projections.bean(DTO.class, column...): getter & setter 주입
- Projections.fileds(DTO.class, column...): 리플렉션(Reflection) 주입 (기본 생성자 public 필수)
- Projections.constructor(DTO.class, column...): 생성자 주입
기본 예제
@Data
@NoArgsConstructor
public class MemberDto {
private Long memberId;
private String name;
public MemberDto(Long memberId, String name) {
this.memberId = memberId;
this.name = name;
}
}
// getter, setter 주입
List<MemberDto> property = query
.select(Projections.bean(MemberDto.class,
member.id.as("memberId"), // 별칭이 다를 경우
member.name))
.from(member)
.fetch();
// field 주입
List<MemberDto> field = query
.select(Projections.fields(MemberDto.class,
member.id.as("memberId"), // 별칭이 다를 경우
member.name))
.from(member)
.fetch();
// 생성자 주입
List<MemberDto> constructor = query
.select(Projections.constructor(MemberDto.class,
member.id,
member.name))
.from(member)
.fetch();
프로퍼티(getter & setter), 필드(fileds) 접근 생성 방식에어 이름이 다른 경우
- ExpressionUtils.as(sorce, alias): 필드나, 서브 쿼리 별칭에 적용
- name.as(alias): 필드에 별칭 적용
// ExpressionUtils.as(source, alias): 필드나, 서브 쿼리 별칭 적용 방법
// name.as("username"): 필드에 별칭 적용
List<MemberDto> result = query
.select(Projections.fields(MemberDto.class,
member.name.as("name"),
ExpressionUtils.as(JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")))
.from(member)
.fetch();
@QueryProjection
Querydsl이 지원하는 Q Class를 만드는 방식으로 complie 단계에서 타입을 체크할 수 있는 장점을 가졌으나, DTO에 Querydsl 애노테이션을 유지해야하는 점과 Q Class를 생성해야하는 단점을 가진다. (의존성 증가)
- Q Class(QMemberDto) 생성: Gradle tab → Tasks → other → compileJava
@Data
@NoArgsConstructor
public class MemberDto {
private Long memberId;
private String name;
@QueryProjection // 생성자에 @QueryProjection
public MemberDto(Long memberId, String name) {
this.memberId = memberId;
this.name = name;
}
}
List<MemberDto> result = query
.select(new QMemberDto(member.id, member.name))
.from(member)
.fetch();
수정, 삭제 벌크 연산
벌크 연산 주의점
벌크 연산 후, 엔티티를 조회하면 영속성 컨텍스트에 남아있는 경우 컨텍스트를 통해엔티티를 조회하게 됩니다. 벌크 연산은 DB 반영 외 영속성 컨텍스트를 건들이지 않기 때문에 영속성 컨텍스트를 초기화하는 것이 안전합니다.
// 28살 미만 회원은 이름을 "비회원"으로 변경
long count = query
.update(member)
.set(member.name, "비회원")
.where(member.age.lt(25))
.execute();
// 전체 회원의 나이를 1살 더하기 (빼기, 곱하기 등등)
long count = query
.update(member)
.set(member.age, member.age.add(1))
// .set(member.age, member.age.add(-1)) // -1
// .set(member.age, member.age.multiply(2)) // x2
.execute();
// 회원 중 18살 초과일 경우 삭제
long count = query
.delete(member)
.where(member.age.gt(18))
.execute();
SQL fuction
SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있습니다.
// 조회 시 "member" -> "user" 로 replace
String fuc = "function('replace', {0}, {1}, {2})";
List<String> result = query
.select(Expressions.stringTemplate(fuc, member.name, "member", "user"))
.from(member)
.fetch();
// 소문자로 변경하여 동일한 이름 조회 (lower)
String findName = "MEmber1"; // member1
String fuc = "function('lower', {0})";
List<String> result = query
.select(member.name)
.from(member)
.where(member.name.eq(Expressions.stringTemplate(fuc, member.name))
.fetch();
lower 같은 ansi 표준 함수의 경우 querydsl이 상당 부분 내장하고 있어 다음과 같이 사용할 수 있습니다.
.where(member.name.eq(member.name.lower()))
동적 쿼리
- BooleanBuilder
- Where 다중 파라미터
BooleanBuilder
@Test
void dynamicQueryBooleanBuilder() {
final String nameParam = "member";
final Integer ageParam = 10;
// dynamic query - BooleanBuilder
List<Member> members = searchBooleanBuilderByMember(nameParam, ageParam);
}
private List<Member> searchBooleanBuilderByMember(String nameCond, Integer ageCond) {
BooleanBuilder builder = new BooleanBuilder();
if (nameCond != null) {
builder.and(member.name.like("%" + nameCond + "%"));
}
if (ageCond != null) {
builder.and(member.age.eq(ageCond));
}
return query
.selectFrom(member)
.where(builder)
.fetch();
}
Where 다중 파라미터
- where 조건에 null 값은 무시
- 해당 방식은 메서드를 다른 쿼리에 재활용 할 수 있다. (nameEq, ageEq → allEq 조합 가능)
@Test
void dynamicQueryWhereParam() {
final String nameParam = "member";
final Integer ageGoeParam = 10;
final Integer ageLoeParam = 20;
List<MemberDto> memberDtos = searchWhereParamByMemberDto(
nameParam, ageGoeParam, ageLoeParam);
}
private List<MemberDto> searchWhereParamByMemberDto(
String nameCond, Integer ageGoeCond, Integer ageLoeCond) {
return query
.select(Projections.constructor(MemberDto.class,
member.id.as("memberId"),
member.name,
member.age))
.from(member)
.where(
nameEq(nameCond),
ageBetween(ageGoeCond, ageLoeCond)
)
.fetch();
}
// Expressions 조합 (NullPointException 처리)
private BooleanExpression combineExpressions(BooleanExpression... expressions) {
BooleanExpression result = Expressions.asBoolean(true).isTrue();
for (BooleanExpression expression : expressions) {
if (expression != null) {
result = result.and(expression);
}
}
return result;
}
private BooleanExpression nameEq(String nameCond) {
return nameCond == null ? null : member.name.eq(nameCond);
}
private BooleanExpression ageBetween(Integer ageGoe, Integer ageLoe) {
return combineExpressions(ageGoe(ageGoe), ageLoe(ageLoe));
}
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;
}
예제 코드
'Spring > JPA' 카테고리의 다른 글
엔티티(Entity) 설계시 주의점 (1) | 2024.05.23 |
---|---|
N+1 문제 (0) | 2024.04.28 |
영속성 전이(CASCADE)와 고아 객체(ORPHAN) (0) | 2024.04.28 |
즉시 로딩(Eager)과 지연 로딩(Lazy) (0) | 2024.04.28 |
프록시(Proxy) (0) | 2024.04.28 |