김영한님의 실전! querydsl 강의 내용을 정리한 노트입니다. 블로그에 있는 자료를 사용하실 때에는 꼭 김영한님 강의 링크를 남겨주세요!
List<String> username = queryFactory
.select(member.username)
.from(member)
.fetch();
@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);
}
}
@Data
@NoArgsConstructor //기본 생성자는 꼭 있어야 함
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
@Test
public void findDtoByJPQL() {
//new 해서 패키지명 다 적어줌 -> 마치 생성자를 호출하듯이 사용, JPQL에서 제공하는 new operation 문법
//이렇게 단순한 쿼리문 하나 짜는데도 좀 별로라는 게 느껴짐
List<MemberDto> resultList = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
.getResultList();
for (MemberDto memberDto : resultList) {
System.out.println("memberDto : " + memberDto);
}
}
순수 JPA에서 DTO를 조회할 때는 new 명령어를 사용
DTO의 package 이름을 다 적어줘야 해서 지저분함 + 생성자 처럼 사용하는 방식만 지원
querydsl은 이러한 한계를 극복한 방법들을 제시함
프로퍼티 접근 - setter
@Test
public void findDtoBySetter() {
//Projections.bean 안에 반환하고자 하는 타입, 프로젝션할 항목들을 쭉 써주면 됨
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);
}
}
com.querydsl.core.types.ExpressionException: study.querydsl.dto.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);
}
}
필드 접근 시 getter, setter가 dto 내부에 정의되어 있지 않아도 사용 가능 → dto의 필드에 값을 바로 뿌려줌
필드명이 일치해야 함 → 에러는 나지 않지만 빈 값이 들어올 수 있음 → UserDto의 name이 null로 들어오는 것을 볼 수 있음
@Data
@NoArgsConstructor
public class UserDto {
private String name;
private int age;
public UserDto(String name, int age) {
this.name = name;
this.age = age;
}
}
@Test
public void findUserDto() {
//Projections.bean 안에 반환하고자 하는 타입, 프로젝션할 항목들을 쭉 써주면 됨
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username, member.age))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto : " + userDto);
}
}
만약 필드명이 다를 경우 어떻게 처리하면 될까? → as를 붙여줘서 dto의 속성명으로 입력해주면 됨
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
member.age))
.from(member)
.fetch();
생성자 사용
@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);
}
}
결국 Projections의 뒷부분을 어떻게 쓰느냐임 → 쓰는 방법은 같지만 실제 dto를 생성해서 값을 넣는 과정이 조금씩 다르다는 것
@Data
@NoArgsConstructor //기본 생성자는 꼭 있어야 함
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
@QueryProjection
만 달아주면 됨 → gradle 컴파일을 해주면 QMember처럼 QMemberDto가 생긴 것을 볼 수 있음 @Test
public void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto : " + memberDto);
}
}
constructor는 컴파일 오류를 잡아내지 못함
List<MemberDto> result = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age,
member.id))
.from(member)
.fetch();
위의 예시처럼 member.id는 프로퍼티로도 없고 생성자로 받지도 않는데 실행이 정상적으로 됨 → 즉, 컴파일 시점에서 이 녀석이 잘못 되었다는 것을 잡아낼 수 없음 → 런타임 시점에서야 문제를 찾을 수 있음
반면 @QueryProjection
은 잘못된 인자가 넘어오면 컴파일 시점에서 문제를 잡아냄
실제 호출하더라도 생성자가 그대로 호출되는 것을 보장해줌
@QueryProjection
어노테이션이 추가되면서 dto가 querydsl에 대해 의존성을 가지게 됨@QueryProjection
을 사용하기 힘듦. 반면 편의성 및 의존성을 감안하고 쓴다면 @QueryProjection
은 꽤 매력적인 선택지@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) {
//usernameCond이 null이 아니면 builder 조건에 username이 usernameCond과 같은지 판별 조건 추가
builder.and(member.username.eq(usernameCond));
}
if (ageCond != null) {
//ageCond가 null이 아니면 builder에 조건 추가
builder.and(member.age.eq(ageCond));
}
return queryFactory
.selectFrom(member)
.where(builder)
.fetch();
}
Integer ageParam = null;
BooleanBuilder builder = new BooleanBuilder(member.username.eq(usernameCond));
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
// 만약 usernameEq가 null을 반환했다면 where절은 이렇게 되겠지?
//where(null, age(ageCond))
//그런데 기본 문법에서 살펴 봤듯이 where에 null이 넘어가면 null 조건은 무시해버림
//null에 대해서는 아무것도 수행하지 않기 때문에 동적 쿼리가 만들어짐!
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameCond), ageEq(ageCond))
.fetch();
}
private Predicate usernameEq(String usernameCond) {
if (usernameCond == null) {
return null;
}
return member.username.eq(usernameCond);
//이렇게 간단한 조건은 삼항 연산자를 사용하면 편리함
// return usernameCond != null ? member.username.eq(usernameCond) : null;
}
private Predicate ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(allEq(usernameCond, ageCond))
.fetch();
}
private Predicate allEq(String usernameCond, Integer ageCond) {
//이렇게 and로 묶어서 한 방에 조건을 쏠 수 있음
return usernameEq(usernameCond).and(ageEq(ageCond));
}
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;
}
@Test
@Commit
public void bulkUpdate() {
//회원의 나이가 20살 이하면 회원 이름을 다 "비회원"으로 변경하기 -> member1, member2가 변경
//처리한 결과로 리턴되는 long 타입(count)는 변경이 완료된 row의 수를 의미
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.loe(20))
.execute();
}
이 상태에서 queryFactory.select로 값을 가져오게 되면 어떻게 될까?
//영속성 컨텍스트 username : DB username
//meber1 : member1
//meber2 : member2
//meber3 : member3
//meber4 : member4
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.loe(20))
.execute();
//meber1 : 비회원
//meber2 : 비회원
//meber3 : member3
//meber4 : member4
//지금 영속성 컨텍스트와 DB의 상태가 일치하지 않는 상태에서 DB를 조회하게 된다면?
List<Member> result = queryFactory
.selectFrom(member)
.fetch();
for (Member member : result) {
System.out.println("member : " + member);
}
JPA는 기본적으로 DB에서 가져온 값을 영속성 컨텍스트에 다시 넣어줌 → 그런데 영속성 컨텍스트와 DB의 상태가 일치하지 않은 상태에서 가져온 DB의 값을 다시 영속성 컨텍스트에 넣으려고 함 → 그런데 영속성 컨텍스트가 이미 값을 들고 있기 때문에 JPA는 DB에서 select 해온 값들을 버림 → DB에 업데이트 친 값들은 버려지게 되고 영속성 컨텍스트에 이전에 들고 있던 값들이 남아있게 됨 → DB에서 select를 해와도 영속성 컨텍스트가 항상 우선권을 가짐 → 값을 DB에 있는 것으로 엎어치지 않는 참사가 발생
결과를 보자! → DB에는 값을 업데이트 쳤지만 영속성 컨텍스트의 값으로 출력되는 것을 확인할 수 있음
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.loe(20))
.execute();
//맘 편하게 영속성 컨텍스트를 비워주자
em.flush();
em.clear();
List<Member> result = queryFactory
.selectFrom(member)
.fetch();
@Test
public void bulkAdd() {
// 모든 회원의 나이를 한 살씩 더하기
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1))
// .set(member.age, member.age.multiply(2)) 이건 곱하기
.execute();
}
@Test
public void sqlFunction() {
//member 라는 단어를 M으로 바꿔서 조회할 예정
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);
}
}
그런데…. 동작을 안함ㅠㅠㅠㅠ
지금 내가 사용하고 있는 DB는 MariaDB이고 MariaDB의 Dialect에 replace가 없다는 것. 한 번 살펴보러 가자!
MariaDBDialect는 MySQL5Dialect를 상속 받고 있고 → MySQL5Dialect는 MySQLDialect를 상속 받고 있음
그런데 MySQLDialect에는 replace 함수가 등록되어 있지 않음 → 그러니 replace를 아무리 불러도 사용할 수 없고 오류가 발생했던 것