프로젝션과 결과 반환 - 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);
}
}
이전에 사용한 바 있는 JPQL을 사용해 DTO로 변환해 조회하는 코드이다. 해당 코드는 new Operation을 사용해야하기 때문에 DTO의 package 명을 다 적어줘야 한다. 이때문에 코드가 지저분해지고, 코드 작성에도 불편함이 있다. 또한, 생성자 방식만 지원하는 불편함도 있다.
하지만, querydsl은 위의 문제를 깔끔하게 해결하는 방법을 제시한다.
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, // getter, setter 상관없이 값을 넣는다.
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);
}
}
필드 값이 다른 dto 접근 - 프로퍼티 접근법만 예시 (필드, 생성자 접근 동일)
@Test
public void findUserDtoBySetter() {
List<UserDto> result = queryFactory
.select(Projections.bean(UserDto.class, //setter를 통해 들어간다.
member.username.as("name"),
member.age))
.from(member)
.fetch();
for (UserDto memberDto : result) {
System.out.println("MemberDto = " + memberDto);
}
}
.as("바꾸고싶은 필드명")으로 필드명을 바꿔주면 된다.
서브쿼리 추가 dto 접근 - 필드 접근법만 예시 (프로퍼티, 생성자 접근 동일)
@Test
public void findUserDtoByFieldSubQuery() {
QMember memberSub = new QMember("memberSub");
List<UserDto> result = queryFactory
.select(Projections.fields(UserDto.class, // getter, setter 상관없이 값을 넣는다.
member.username.as("name"),
Expressions.as(JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
))
.from(member)
.fetch();
for (UserDto userDto : result) {
System.out.println("userDto = " + userDto);
}
}
- ExpressionUtils.as(source, alias) : 필드나 서브쿼리에 별칭 적용
프로젝션과 결과 반환 - @QueryProjection
@QueryProjection을 사용하기 위해서는 준비 작업이 필요합니다.
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
@QueryProjection
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
사용하고자하는 dto 클래스의 생성자에 @QueryProjection을 추가해줍니다. 실행을 한번 해보거나 이전에 돌렸던 compilequerydsl을 눌러줍니다. 그렇게되면 @MemberDto가 생성됩니다. 이제 사용해봅시다.
생성자 + @QueryProjection
@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);
}
}
생성자 dto 보다의 장점은 컴파일 시점에 오류를 잡아준다는 것 입니다. 생성자 dto는 런타임 시점에 오류를 잡아주지만, 생성자+@QueryProjection은 컴파일 시점에 오류를 잡아줍니다.
위 @QueryProjection은 다 좋지만 2가지 단점이 존재합니다.
- QMemberDto와 같이 Q멤버를 만들어야 해야 합니다.
- Q멤버로 만드는 경우, dto는 querydsl에 의존성을 가지게 됩니다.
- 해당 문제는 아키텍쳐 관점에서 문제가 생긴다고 한. 해당 dto를 repository, service, application 계층 모두 사용하는데 querydsl에 의존적인 dto를 모든 계층에서 사용하는데 어떠한 문제가 있을지는 아직 모르겠다,,
distinct
@Test
public void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age)).distinct()
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
이렇게 select뒤에 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();
}
위와 같이 null이 아닌 경우에만 builder.and(where안에 넣을 조건)을 넣어주면 된다.
동적 쿼리 - Where 다중 파라미터 사용
김영한님이 즐겨쓰는 방식이라고 한다.
@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))
.fetch();
}
private Predicate usernameEq(String usernameCond) {
return usernameCond == null ? null : member.username.eq(usernameCond);
}
private Predicate ageEq(Integer ageCond) {
return ageCond == null ? null : member.age.eq(ageCond);
}
이 기능의 장점은 메소드라 빠지기 때문에, 아래와 같이 조립이 가능하다.
@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))
.fetch();
}
private BooleanExpression usernameEq(String usernameCond) {
return usernameCond == null ? null : member.username.eq(usernameCond);
}
private BooleanExpression ageEq(Integer ageCond) {
return ageCond == null ? null : member.age.eq(ageCond);
}
private BooleanExpression allEq(String usernameCond, Integer ageCond){
return usernameEq(usernameCond).and(ageEq(ageCond));
}
- where 조건에 null 값은 무시된다.
- 메서드를 다른 쿼리에서도 재활용 할 수 있다. (이 점이 정말 좋은 것 같다.)
- 쿼리 자체의 가독성이 높아진다. (이전 BooleanBuilder보다 가독성이 뛰어나다.)
수정, 삭제 배치 쿼리
쿼리 한번으로 대량 데이터 수정
@Test
public void bulkUpdate() {
// member1 = 10 = > 비회원
// member2 = 20 = > 비회원
// member1 = 30 = > 그대로
// member2 = 40 = > 그대로
long count = queryFactory
.update(member)
.set(member.username, "비회원")
.where(member.age.lt(28))
.execute();
}
두 컬럼의 데이터를 update했지만, 쿼리는 한번만 나간 것을 볼 수 있다.
참고
벌크 연산은 이전에도 말했지만, 조심해야할 것이 있다.
영속성 컨텍스트를 무시하고 DB에 직접 접근해서 update 쿼리문을 날리기 때문이다. 이것이 문제가 되는 것은 영속성 컨텍스트에 update쿼리문에 날라간 데이터가 있고, 벌크 연산 후 데이터를 가져오면 DB가 아닌 영속성 컨텍스트에서 가져오기 때문에 벌크연산된 데이터가 아닌 이전의 데이터를 가져온다. 그렇기 때문에 벌크 연산후에는 em.clear()를 해주자.
쿼리 한번으로 대량 데이터 추가, 곱, 삭제
@Test
public void bulkAdd() {
long count = queryFactory
.update(member)
.set(member.age, member.age.add(1)) //곱하기는 add 대신 multiply
.execute();
}
@Test
public void bulkDelete() {
long count = queryFactory
.delete(member)
.where(member.age.gt(18))
.execute();
}
해당 벌크 연산도 벌크 연산 후 em.clear()를 필수로 해주어야 한다.
SQL function 호출하기
SQL function은 JPQ와 같이 Dialect에 등록된 내용만 호출할 수 있다.
아래 예시는 member string을 m string으로 replace하는 함수를 사용한 예제이다.
@Test
public void slqFunction() {
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("result = " + s);
}
}
replace말고도 소문자로 변환하는 경우는 아래예시이다.
@Test
public void sqlFunction2() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.where(member.username.eq(Expressions.stringTemplate("function('upper', {0})", member.username)))
//.where(member.username.eq(member.username.upper()))
.fetch();
for (String s : result) {
System.out.println("result = " + s);
}
}
비교할 데이터를 모두 소문자로 비교한 뒤, 비교할 때는 위와같이 소문자로 변경한 뒤 비교할 수 있다.
참고로, lower 같은 ansi 표준 함수들은 querydsl이 상당부분 내장하고 있다. 따라서 아래와 같이 처리해도 결과는 같다.
.where(member.username.eq(member.username.upper()))
출처
'spring > 인프런 강의 정리' 카테고리의 다른 글
[Querydsl] 5. 실무 활용 - 스프링 데이터 JPA와 Querydsl (+페이징) (0) | 2022.05.20 |
---|---|
[Querydsl] 4. 실무 활용 - 순수 JPQ와 Querydsl (0) | 2022.05.19 |
[Querydsl] 2. 기본 문법 (0) | 2022.05.17 |
[Querydsl] 1. 프로젝트 세팅 (0) | 2022.05.17 |
[Spring Data JPA] 4. 확장 기능 (0) | 2022.05.16 |