프로젝션과 결과 반환 - 기본
•
프로젝션 : select 대상 지정
프로젝션 대상이 하나
@Test
public void simpleProjection() throws Exception {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
Java
복사
•
프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음
•
프로젝션 대상이 둘 이상이면 튜플이나 DTO로 조회
튜플 조회
@Test
public void tupleProjection() {
List<Tuple> result = queryFactory
.select(member.username, member.age) // 여러개가 넘어오는 경우
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("tuple.get(member.username) = " + tuple.get(member.username));
System.out.println("tuple.get(member.age) = " + tuple.get(member.age));
}
}
Java
복사
•
튜플이 Repository 계층을 넘어서 Service, Controller까지 침범하는 것은 좋지 않음 (상위 구현이 하위 구현에 의존)
•
튜플은 Querydsl 종속적인 Type이기 때문에, Repository 내에서만 사용 → 그 이외의 Layer에 영향을 줄 때에는 DTO로 변환해서 사용
프로젝션 결과 반환 - DTO 조회
순수 JPA에서 DTO 조회 코드
@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);
}
}
Java
복사
•
순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용해야함
•
DTO의 package 이름을 다 적어줘야해서 코드가 지저분함
•
생성자 방식만 지원함
Querydsl 빈 생성(Bean popluation)
결과를 DTO로 반환할 때 사용, 아래 세가지 방법
1.
프로퍼티 접근
@Test
public void findDtoBySetter() {
List<MemberDto> result = queryFactory
.select(Projections.bean(MemberDto.class, // 기본생성자, Setter, Getter 필요 (Dto 생성 시)
member.username,
member.age))
.from(member) //QMember
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
Java
복사
2.
필드 직접 접근
@Test
public void findDtoByField() {
List<MemberDto> result = queryFactory
.select(Projections.fields(MemberDto.class, // getter, setter가 없어도 됨, 필드로 바로 주입 (property가 일치해야함)
member.username,
member.age))
.from(member) //QMember
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
Java
복사
3.
생성자 사용
@Test
public void findDtoByConstructor() {
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class, // 생성자의 type이 정확히 맞아야함 (field값이 아님!)
member.username,
member.age))
.from(member) //QMember
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
Java
복사
•
프로퍼티나, 필드 접근 생성 방식에서 이름이 다를 때
@Test
public void findUserDto() {
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"), // .as("field명") - Dto class Property를 명시
ExpressionUtils.as(JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age") // alias를 줘서 subquery의 결과가 field값(age)에 매치되도록
))
.from(member) //QMember
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
Java
복사
◦
ExpressionUtils.as(source,alias) : 필드나, 서브 쿼리에 별칭 적용
◦
username.as("memberName") : 필드에 별칭 적용
프로젝션과 결과 반환 - @QueryProjection
생성자 + @QueryProejction
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
Java
복사
•
./gradlew compileQuerydsl
◦
QMemberDto 생성 확인
•
@QueryProjection 활용
@Test
public void findDtoByQueryProjection() {
// Dto 자체가 Querydsl 라이브러리의 영향을 받게됨 (순수하지 않은 Dto)
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
}
Java
복사
◦
이 방법은 컴파일러로 타입을 체크할 수 있으므로 가장 안전한 방법이다. 다만 DTO에 Querydsl 어노테이션을 유지해야하는 점과 DTO까지 Q파일을 생성해야하는 단점이 있다.
동적쿼리
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)); //usernameCond에 값이 있으면, and 조건 추가
}
if (ageCond != null) {
builder.and(member.age.eq(ageCond));
}
return queryFactory
.selectFrom(member)
.where(builder) // and, or로 조립 가능!
.fetch();
}
Java
복사
Where 다중 파라미터 사용
•
where 조건에 null값은 무시된다.
@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(usernameEq(usernameCond), ageEq(ageCond)) // where의 조건이 null이면 무시됨
.fetch();
}
private Predicate ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null; // 삼항 연산자 사용
}
private Predicate usernameEq(String usernameCond) {
if (usernameCond == null) return null;
return member.username.eq(usernameCond);
}
Java
복사
•
Composition
◦
다른 쿼리에서 재사용이 가능
◦
쿼리 자체의 가독성이 높아진다.
◦
BooleanExpression을 return값으로 사용
@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(allEq(usernameCond, ageCond)) // where의 조건이 null이면 무시됨
.fetch();
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null; // 삼항 연산자 사용
}
private BooleanExpression usernameEq(String usernameCond) {
if (usernameCond == null) return null;
return member.username.eq(usernameCond);
}
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
Java
복사
주의! null 체크는 주의해서 해야한다.
수정, 삭제 배치 쿼리 (Bulk 연산)
쿼리 한번으로 대량 데이터 수정
•
Bulk 연산 실행 후 영속성 컨텍스트를 초기화해야한다.
@Test
@Commit
public void bulkUpdate() {
//member1 = 10 -> DB member1
//member2 = 20 -> DB member2
//member3 = 30 -> DB member3
//member4 = 40 -> DB member4
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute(); // bulk 연산은 영속성 컨텍스트를 무시하고 바로 DB를 update, DB <> 영속성 컨텍스트 (Repeatable read)
//member1 = 10 -> DB 비회원
//member2 = 20 -> DB 비회원
//member3 = 30 -> DB member3
//member4 = 40 -> DB member4
em.flush();
em.clear(); // bulk 연산 후 영속성 컨텍스트 초기화
List<Member> result = queryFactory
.selectFrom(member)
.fetch();
for (Member member1 : result) {
System.out.println("member1 = " + member1);
}
}
Java
복사
기존 숫자에 1 더하기
@Test
public void bulkAdd() {
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1))
.execute();
}
Java
복사
•
곱하기
@Test
public void bulkMultiply() {
long count = queryFactory
.update(member)
.set(member.age, member.age.multiply(2))
.execute();
}
Java
복사
삭제
@Test
public void bulkDelete() {
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
}
Java
복사
SQL function 호출하기
•
SQL function은 JPA와 같이 Dialect에 등록된 내용만 호출할 수 있다.
◦
“member”를 “m”으로 변경하는 함수
@Test
public void sqlFunction() {
List<String> result = queryFactory
.select(
Expressions.stringTemplate(
"function('replace', {0}, {1}, {2})",
member.username, "member", "m"))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
Java
복사
◦
소문자로 변경해서 비교해라.
@Test
public void sqlFunction2() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.where(member.username.eq(
Expressions.stringTemplate("function('lower', {0})", member.username)))
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
Java
복사
lower()같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있다.
따라서 다음과 같이 처리해도 결과는 같다.
.where(member.username.eq(member.username.lower()))
Java
복사