[QureyDsl] 설정& 문법 & 오류 정리 본문

백앤드 개발일지/스프링부트

[QureyDsl] 설정& 문법 & 오류 정리

giron 2021. 9. 2. 20:31
728x90

[build.gradle]

plugins {
    id 'org.springframework.boot' version '2.3.1.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    //querydsl 추가
    id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    id 'java'
}

group = 'study'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.6.1'
    //querydsl 추가
    implementation 'com.querydsl:querydsl-jpa'

    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    runtimeOnly 'com.h2database:h2'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

test {
    useJUnitPlatform()
}
//querydsl 추가 시작
def querydslDir = "$buildDir/generated/querydsl"
querydsl {
    jpa = true
    querydslSourcesDir = querydslDir
}
sourceSets {
    main.java.srcDir querydslDir
}
configurations {
    querydsl.extendsFrom compileClasspath }
compileQuerydsl {
    options.annotationProcessorPath = configurations.querydsl
}
//querydsl 추가 끝

🔊JPAQueryFactory 스프링 빈 등록

@Configuration
public class QuerydslConfig {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

[QEntity 생성]

✒️IntelliJ base Gradle을 기준

1. Gradle → Tasks → build → clean

2. Gradle → Tasks → other → compileQuerydsl

Gradle 콘솔 사용법

  • ./gradlew clean compileQuerydsl

사용자 정의 리포지토리

사용자 정의 리포지토리 사용법

1. 사용자 정의 인터페이스 작성

2. 사용자 정의 인터페이스 구현

3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

 

1. 사용자 정의 인터페이스 작성

public interface MemberRepositoryCustom {
    List<Member> findMemberCustom();
}

 

2. 사용자 정의 인터페이스 구현

public class MemberRepositoryImpl implements MemberRepositoryCustom{

    @Override
    public List<Member> findMemberCustom() {
        return null;
    }

    private final JPAQueryFactory queryFactory;

    public MemberRepositoryImpl(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }

    @Override
    public List<MemberTeamDto> search(MemberSearchCondition condition) {
        return queryFactory
                .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())
                )
                .fetch();
    }

    private BooleanExpression usernameEq(String username) {
        return isEmpty(username)?null:member.username.eq(username);
    }
    private BooleanExpression teamNameEq(String teamName) {
        return hasText(teamName) ? team.name.eq(teamName):null;
    }

    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;
    }
}

 

3. 스프링 데이터 리포지토리에 사용자 정의 인터페이스 상속

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {

    List<Member> findByUsername(String username);
}

4. 사용

memberRepository.search(condition);

 

2. (Optional) 쿼리 파라미터 로그 남기기

1. 로그에 다음을 추가하기 org.hibernate.type : SQL 실행 파라미터를 로그로 남긴다.

2. 외부 라이브러리 사용 → https://github.com/gavlyukovskiy/spring-boot-data-source-decorator

→ Gradle 에 아래 내용 추가

implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.6.1'

 

참고: 쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로, 개발 단계에서는 편하게 사용해도 되지만, 운영시스템에 적용하는 시점에서는 성능테스트를 해보고 결정하는 것이 좋다.

 

[서브쿼리 서브스트링해서 작업할때]

무턱대로 account.email.length() 에 -1 하지 마시고 subtract 이거 써서 account.email.length().subtract(2) 하세요!!

 

[linux 환경변수 영구 적용]

https://devpouch.tistory.com/125

 

[linux] 환경변수 설정, 확인 및 해제 명령어

리눅스 환경변수를 적용하기 위해서는 크게 일시적으로 적용하는 방법과 영구적으로 적용하는 방법으로 나뉜다. 아래 내용은 bash 쉘 기준으로 작성되었다. 리눅스 환경변수 일시 적용 $ export 환

devpouch.tistory.com

[문법]

  • member.username.eq("a") : username = 'a'
  • member.username.ne("a") : username ≠ 'a'
  • member.username.eq("a").not() : username ≠ 'a'
  • member.username.isNotNull() : username 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) : age between 10, 30
  • member.age.goe(30) : age ≥ 30
  • member.age.loe(30) : age ≤ 30
  • member.age.lt(30) : age < 30
  • member.age.gt(30) : age > 30
  • member.username.like("member%") : username like 'member%'
  • member.username.startsWith("member") : like 'member%'
  • member.username.contains("member') : username like '%member%'

[결과반환 함수]

  • fetch() : 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne() : 단 건 조회
    • 결과가 없으면: null
    • 결과가 둘 이상이면: com.querydsl.core.NonUniqueResultException
  • fetchFirst() : limit(1).fetchOne() 과 같다.
  • fetchResults() : 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount() :count 쿼리로 변경해서 count 수 조회

[정렬]

.orderBy(인스턴스명.기준필드.정렬기준.(nullsLast()|nullsFirst()))

  • desc(), asc() :일반 정렬
  • .orderBy(post.doLocalDate.desc()); : 날짜 내림차순이면 최근 날짜가 우선 나온다. ex) 10-03, 10-02, 09-30 ...
  • nullsLast(), nullsFirst() :null 데이터 순서 부여

[페이징]

페이징할 때는 orderBy를 넣어서 정렬을 해줘야 잘 동작한다.

offset(1).limit(2)는 index(0)을 생략하고 두개를 선택한다는 뜻 → [0][1][2][3]

@Test
public void paging2() throws Exception {
    QueryResults<Member> result = queryFactory
            .selectFrom(member)
            .orderBy(member.username.desc())
            .offset(1)
            .limit(2)
            .fetchResults();

    assertThat(result.getTotal()).isEqualTo(4);
    assertThat(result.getLimit()).isEqualTo(2);
    assertThat(result.getOffset()).isEqualTo(1);
    assertThat(result.getResults().size()).isEqualTo(2);
}

[content와 count 분리]

전체 카운트를 한번에 조회 : fetchResults

  • @Override
    public Page<MemberTeamDto> searchPageSimple(MemberSearchCondition condition, Pageable pageable) {
        
        QueryResults<MemberTeamDto> results = queryFactory
                .select(Projections.constructor(MemberTeamDto.class,
                        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())
                .fetchResults();
        
        List<MemberTeamDto> content = results.getResults();
        long total = result.getTotal();
        
         // 기존 방법
         // return new PageImpl<>(content, pageable, total); 
         //스프링데이터 라이브러리 사용
         return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchCount);
  • 실제 쿼리는 2번 호출된다.
  • fetchResults()는 카운트 쿼리 실행시 필요없는 order by는 제거한다.
  • PageableExectutionUtils
    • count 쿼리가 생략 가능한 경우 생략해서 처리한다.
      • 페이지 시작이면서 컨텐츠 사이즈가 페이지 사이즈 보다 작을 때
      • 마지막 페이지 일때 (offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구함)

데이터 내용과 전체카운트를 별도로 조회하는 방법

    •  
@Override
public Page<MemberTeamDto> searchPageComplex(MemberSearchCondition condition, Pageable pageable) {
    
    List<MemberTeamDto> content = queryFactory
            .select(Projections.constructor(MemberTeamDto.class,
                    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();
            
    long total = queryFactory
            .select(member)
            .from(member)
            .leftJoin(member.team, team)
            .where(usernameEq(condition.getUsername()),
                    teamNameEq(condition.getTeamName()),
                    ageGoe(condition.getAgeGoe()),
                    ageLoe(condition.getAgeLoe()))
            .fetchCount();    
            
    return new PageImpl<>(content, pageable, total); 
}
  • Join이 복잡할 경우는 이 방법을 사용한다. 
  • 코드를 리팩토링해서 내용 쿼리와 전체 카운트 쿼리를 읽기 좋게 분리하면 좋다.

 

[정렬]PageDto
@Controller

  • import lombok.Getter; 
            import org.springframework.data.domain.PageRequest; 
            import org.springframework.data.domain.Pageable; 
            import org.springframework.data.domain.Sort;
            
            @Getter
            public class PageDto {
                private int page;
                private int size;
                private String sort;
    
                public PageDto(int page, int size) {
                    this.page = page;
                    this.size = size;
                    this.sort = "default";
                }
    
                public void setSort(String sort) {
                    this.sort = sort;
                }
    
                public void setPage(int page) {
                    this.page = page <= 0 ? 1 : page;
                }
    
                public void setSize(int size) {
                    int DEFAULT_SIZE = 10;
                    int MAX_SIZE = 50;
                    this.size = size > MAX_SIZE ? DEFAULT_SIZE : size;
                }
    
                public Pageable of() {
                    return PageRequest.of(this.page - 1, this.size, Sort.by(Sort.Order.desc(sort)));
                }
            }
  • @GetMapping("/{businessProfileId}/posts")
        public ResponseEntity<Page<BusinessProfileResponse.PostList>> postList(@PathVariable Long businessProfileId,
                                                                               PageDto pageDto){
            Page<BusinessProfileResponse.PostList> postLists = businessProfileService.postList(businessProfileId, pageDto);
    
            return ResponseEntity.ok().body(postLists);
    @Service
  • public Page<BusinessProfileResponse.PostList> postList(Long businessProfileId, PageDto pageDto){
            Page<Post> posts = postRepository.findAllByBusinessProfileId(businessProfileId, pageDto.of());
            List<BusinessProfileResponse.PostList> postLists = posts.stream().map(BusinessProfileResponse.PostList::build).collect(Collectors.toList());
            return new PageImpl<>(postLists, pageDto.of(), posts.getTotalElements());
    
        }

queryDsl을 이용하여 Result Aggregation 사용하기

groupBy, list, transform 이용

transform을 이용하면 groupBy로 키 값을 정할수 있고 그에 따라 .as뒤에 원하는 리스트 형식이나 다른 컬럼의 형식으로 묶을수 있다. 이때 컬럼을 키값으로 갖기 떄문에 Lazy Loading이 발생한다.

엔티티를 키값으로 가질수도 있다.

 

[QueryDsl로 동적 쿼리 해결 방식]

  • BooleanBuilder
  • Where 다중 파라미터 사용
public List<MemberTeamDto> search(MemberSearchCondition condition) {
    return queryFactory
            .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())
            )
            .fetch();
}
private BooleanExpression usernameEq(String username) {
    return isEmpty(username) ? null : member.username.eq(username);
}

private BooleanExpression teamNameEq(String teamName) {
    return hasText(teamName) ? team.name.eq(teamName) : null;
}

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;
}

위 코드는 where절 사용법인데 아래 장점이 있다.

  • usernameEq() 과 같은 메서드는 다른 쿼리에서 재활용 할 수도 있다.
  • 쿼리 자체의 가독성이 높아진다.

💻TIP 

  •  .and() 로 연결할때 where절의 첫번째 인자에 null이 들어오면 null을 무시 못하고 nullpointerException이 터진다! -> .and()안에 있을때는 또 null을 잘 무시하고 실행 된다.(원래 querydsl은 null이 들어오면 무시하고 넘어가야 함)
  • 그런데 .and()말고 위 예시처럼 , 로 연결을 하면 첫번째 인자가 null이 들어와도 잘 무시한다!!!!!🤓😎💻
  • 정확히 왜 이러는지 아시는 분 있으시면 댓글로 꼭 알려주면 고맙겠습니다😭😭 한참 고민하고 다양한 예시 넣어보다가 발견했슴다.

내 생각

and 함수의 내부

아무래도 and도 매서드이기 때문에 처음 and()를 부르는 객체가 null이면 NullPointerException이 나오는게 아닌가 생각된다. 그렇다면 이후 .and(null)은 무시가 되는게 성립이 된다. (내부에서 null은 @Nullable이기 떄문에) 그래서 처음 객체가 NotNull이어야 한다고 생각한다. 🧐🧐 -해결- (제가 생각한 이유가 맞겠죠..?)

 

fetch join vs join

  • fetch join으로 조인하면 영속상태로 갖고있게 된다. 그래서 찾은 엔티티에서 다른 엔티티의 값이 필요할 떄, fetch join을 사용해야 한다. (한번에 영속상태로 끌어오므로)
  • 패치 조인은 특정 컬럼만 뽑아낼수 없다. 전부 selectFrom 해야 한다.
  • fetch join은 inner join으로 작동한다. 
  • 일반 join은 영속상태로 끌어오지 않으므로 1차 캐시를 이용하지 않을 때 사용하면 좋다. 
  • 위 부분때문에 N+1문제가 발생하는 것!(영속성컨텍스트가 관리하지 않으므로 엔티티를 뽑으려면 다시 쿼리를 날려야 하기 때문에)
  • 또는 특정 컬럼만 뽑아낼때 사용해도 좋다.
  • 일반 조인은 연관된 엔티티를 사용하지 않을때나, 특정 컬럼만 뽑을때 사용하면 좋다.
  • 패치 조인은 연관된 엔티티를 사용할 떄 사용하면 좋다. (N+1문제 해결)
  •   .leftJoin(user.avatar).fetchJoin()와 같이 leftjoin에 패치조인을 적용할 수 있다.

*******************************************************************************************

[where절 vs on절]

  • on 절은 전부 조인 후에 일치하지 않은것은 null값 넣고
  • where절은 먼저 조건을 필터링 한 후에 필터링된 것만 조인한다.

*****************************************************************************************

[페이징 처리할 때, OneToMany 일때는 fetch join을 사용하지 말기.] 

  • Many쪽으로 데이터 뻥튀기기 때문에 1+N문제가 발생한다.
  • user : Team = M : 1 일때,  join(user.team, team).fetchjoin()...
  • ManyToMany일땐, 사용하지 않는다. jpa에서 자동으로 막아준다고는 한다.(너무 많이 땡기면 뻑나기 때문에)
  • ToMany일때는, jpa.properties.hibernate.default_batch_fetch_size:  사용하기

 

Projection을 이용한 처리

쿼리에서 한번에 처리

Projection과 Dto이용

위와 같이 사용하면 map을 사용하지않고 바로 아래처럼 결과가 나올수 있다.

결과

하지만 null값이 들어가는 문제를 아직 해결하지 못했다.

참고: https://bbuljj.github.io/querydsl/2021/05/17/jpa-querydsl-projection-list.html

 

Result Aggregation 사용

import static com.querydsl.core.group.GroupBy.groupBy;
import static com.querydsl.core.group.GroupBy.list;

    public List<Family> findFamily() {
        Map<Parent, List<Child>> transform = queryFactory
                .from(parent)
                .leftJoin(parent.children, child)
                .transform(groupBy(parent).as(list(child)));

        return transform.entrySet().stream()
                .map(entry -> new Family(entry.getKey().getName(), entry.getValue()))
                .collect(Collectors.toList());
    }

transform을 이용해서 map을 반환하도록 만들수도 있다. 그후 stream을 사용해서 활용할수도 있다.

출처: https://jojoldu.tistory.com/342

 

[벌크 연산]

벌크 연산 후 자동으로 초기화 하는 방법: Query위에 애노태이션 넣어주면 된다. Option<T> deleteBy~ 위

 @Modifying(clearAutomatically = true)
  • 특정 컬럼을 변경하고 싶을때 
    • @Test
          public void bulk_update(){
              long count = qf
                      .update(member)
                      .set(member.username, "비회원")
                      .where(member.age.lt(20))
                      .execute();
      
              em.flush();
              em.clear();
      
      
              List<Member> fetch = qf.selectFrom(member).fetch();
              for (Member member : fetch) {
                  System.out.println("member = " + member);
              }
          }
    • 벌크 연산은 영속성컨텍스트를 무시하고 바로 DB로 날리므로 연산 후, 초기화를 해줘야 한다.
    • 그 방법이 update, set, execute(),이다.
  • 특정컬럼에 같은 값을 더하고 싶을때 
    •  @Test
          public void bulkAdd(){
              long execute = qf
                      .update(member)
                      //add , minus , multiple 다 가능하다.
                      .set(member.age, member.age.add(1)) //(1)바꾸고 싶은 값, (2) 바꿀 값
                      .execute();
              em.clear();
              em.flush();
      
              List<Member> fetch = qf.selectFrom(member).fetch();
              for (Member member : fetch) {
                  System.out.println("member = " + member);
              }
          }
  • 한번에 모두 삭제
    •  @Test
          void bulk_delete(){
              long execute = qf
                      .delete(member)
                      .execute();
              em.clear();
              em.flush();
      
              List<Member> fetch = qf.selectFrom(member).fetch();
              for (Member member : fetch) {
                  System.out.println("member = " + member);
              }
          
          }

 

 

728x90
Comments