ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [내일배움캠프] 3월 6일 월요일 TIL 회고록
    카테고리 없음 2023. 3. 6. 22:40

    JPQL이 제공하는 검색 조건

    member.username.eq("member1") // username = 'member1'
    member.username.ne("member1") //username != 'member1'
    member.username.eq("member1").not() // username != 'member1'
    
    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("member%") //like 검색 
    member.username.contains("member") // like ‘%member%’ 검색 
    member.username.startsWith("member") //like ‘member%’ 검색

    eq = 같을때

    !eq  = 같지 않을때

    .not() : 같지 않을때

    isNotNull = 이름이 공백이 아닐때  

    in(10,20) : 나이가 10이거나 20을 찾는 조건

    notIn(10,20) 나이가 10이거나 20이 아닌 나이를 찾는 조건

    beetween(10,30) : 나이가 10이상 30이하를 찾는 조건

    goe : 크거나 같다

    gt : 크다

    loe: 작거나 같다

    lt : 작다

     

    결과 조회

    

        // 리스트로 조회
        List<Member> fetch = queryFactory
            .selectFrom(member)
            .fetch();
    
        // 단건 조회
        Member fetchOne = queryFactory
            .selectFrom(member)
            .fetchOne();
    
        // 처음 한 건 조회
        Member fetchFirst = queryFactory
            .selectFrom(member)
    //        밑에 fetchFirst(); 랑 똑같음
    //       .limit(1).fetchOne();
            .fetchFirst();
    
        // 페이징에서 사용
        QueryResults<Member> results = queryFactory
            .selectFrom(member)
            .fetchResults();
    
        results.getTotal();
        List<Member> content = results.getResults();
    
        // count 쿼리로 변경
        long total = queryFactory
            .selectFrom(member)
            .fetchCount();

     

    복잡하고 성능이 중요한 페이징 쿼리에서는 위에 두 쿼리는 쓰면 안된다. 그때는 쿼리 두번을 따로 날려야 한다.


    정렬 : 

    정렬 기준 : 회원 나이 내림차순으로 정렬(desc), 회원 이름 올림차순으로 정렬(asc), 회원 이름이 없으면 마지막에 출력(nulls last)

    @Test
    public void sort() {
      // 회원 추가, 이름은 null, 나이는 100살
      em.persist(new Member(null,100));
      // 회원 추가, 이름은 member5, 나이는 100살
      em.persist(new Member("member5",100));
      // 회원 추가, 이름은 member6, 나이는 100살
      em.persist(new Member("member6",100));
    
      List<Member> result = queryFactory
      	  // select, from member
          .selectFrom(member)
          // where문을 사용해서 멤버의 나이가 100살인 사람을 찾는다.
          .where(member.age.eq(100))
          // orderBy를 사용해서 멤버의 나이를 내림차순으로 정렬,
          // 멤버의 이름은 오름차순으로 정렬하는데 null값이 있다면 마지막에 출력
          .orderBy(member.age.desc(), member.username.asc().nullsLast())
          // 리스트 조회
          .fetch();
          
      // member5에 result 리스트에 0번째 값을 가져온다    
      Member member5 = result.get(0);
      // member6에 result 리스트에 1번째 값을 가져온다 
      Member member6 = result.get(1);
      // memberNull에 result 리스트에 2번째 값을 가져온다 
      Member memberNull = result.get(2);
      
      // asserThat으로 검증, member5의 이름이 "member5"와 같은지 검증
      assertThat(member5.getUsername()).isEqualTo("member5");
      // asserThat으로 검증, member6의 이름이 "member6"와 같은지 검증
      assertThat(member6.getUsername()).isEqualTo("member6");
      // asserThat으로 검증, memberNull의 이름이 null값을 반환하는지 검증
      assertThat(memberNull.getUsername()).isNull();
    
    }

    페이징 처리

    @Test
    public void paging1() {
      List<Member> result = queryFactory
          .selectFrom(member)
          .orderBy(member.username.desc())
          // offset, limit로 페이징을 지원한다.
          .offset(1)
          .limit(2)
          .fetch();
    
      assertThat(result.size()).isEqualTo(2);
    }

    강의에는 이 후에 fetchResult를 사용해서 한번 더 보여주는데 Querydsl이 5.0이 되면서 fetchResult와 fetchCount가 사라졌다.

    사용하려고 하면 이렇게 노란색으로 표시된다.

    그래서 구글링을 해봤다.

    사라진 이유 :

    원문 

    fetchResults requires a count query to be computed. 
    In querydsl-sql, this is done by wrapping the query in a subquery, like so: SELECT COUNT(*) FROM (<original query>).
    Unfortunately, JPQL - the query language of JPA - does not allow queries to project from subqueries. 
    As a result there isn't a universal way to express count queries in JPQL. 
    Historically QueryDSL attempts at producing a modified query to compute the number of results instead. 
    However, this approach only works for simple queries.
    Specifically queries with multiple group by clauses and queries with a having clause turn out to be problematic.
    This is because COUNT(DISTINCT a, b, c), while valid SQL in most dialects, is not valid JPQL. 
    Furthermore, a having clause may refer select elements or aggregate functions and therefore cannot be emulated by moving the predicate to the where clause instead.
    In order to support fetchResults for queries with multiple group by elements or a having clause, we generate the count in memory instead.
    This means that the method simply falls back to returning the size of fetch(). 
    For large result sets this may come at a severe performance penalty.
    For very specific domain models where fetchResults() has to be used in conjunction with complex queries containing multiple group by elements and/or a having clause, we recommend using the Blaze-Persistence  integration for QueryDSL.
    Among other advanced query features, Blaze-Persistence makes it possible to select from subqueries in JPQL.
    As a result the BlazeJPAQuery provided with the integration, implements fetchResults properly and always executes a proper count query.
    Mind that for any scenario where the count is not strictly needed separately, we recommend to use fetch() instead.

    파파고 번역

    queryDsl의 fetchResult의 경우 count를 하기위해선 count용 쿼리를 만들어서 실행해야 하는데, 카운트를 하려는 select 쿼리를 기반으로 count 쿼리를 만들어 실행한다.
    위의 전문을 보면 이런 식인 것 같다.
    SELECT COUNT(*) FROM (<original query>).

    그런데 이게 단순한 쿼리에서는 잘 동작하는데, 복잡한 쿼리(다중그룹 쿼리)에서는 잘 작동하지 않는다고 한다.

    group by 나 having절을 사용하는 등의 복잡한 쿼리문에서 에러가 출력이 되고 해서

    그냥 fetch() 쓰고 자바쪽에서 count를 세서 사용하라고 한다.


    집합

     

    예시 1

    @Test
    public void aggregation() {
      // 튜플을 쓰는 이유 : 데이터 타입이 여러개가 들어올때 (단일타입이 아닐때) 튜플을 쓴다.
      List<Tuple> result = queryFactory
          .select(
              // 멤버의 총 인원? (카운트)
              member.count(),
              // 멤버의 나이를 더한 값
              member.age.sum(),
              // 멤버의 평균 나이
              member.age.avg(),
              // 멤버중에서 최대 나이
              member.age.max(),
              // 멤버중에서 최소 나이
              member.age.min()
          )
          .from(member)
          .fetch();
    
      // result 리스트의 0번째 값을 가져온다.
      Tuple tuple = result.get(0);
      // member.count가 4개가 맞는지 확인 (before 메소드에 4명 등록돼있음)
      assertThat(tuple.get(member.count())).isEqualTo(4);
      // 멤버의 나이를 합한 값이 100이 맞는지 확인 (10+20+30+40 = 100)
      assertThat(tuple.get(member.age.sum())).isEqualTo(100);
      // 멤버의 평균 나이가 25가 맞는지 확인 ( (10+20+30+40) / 4 = 25 )
      assertThat(tuple.get(member.age.avg())).isEqualTo(25);
      // 멤버중에서 최대 나이가 40이 맞는지 확인 
      assertThat(tuple.get(member.age.max())).isEqualTo(40);
      // 멤버중에서 최소 나이가 10이 맞는지 확인
      assertThat(tuple.get(member.age.min())).isEqualTo(10);
    }

    예시 2 

    조건 : 팀의 이름과 각 팀의 평균 연령을 구하기

    /**
     * 팀의 이름과 각 팀의 평균 연령을 구해라.
     */
    @Test
    public void group() throws Exception {
      List<Tuple> result = queryFactory
          .select(team.name, member.age.avg())
          .from(member)
          .join(member.team, team)
          // groupBy를 팀의 이름으로 했는데 팀의 이름은 두개니까(teamA,teamB) 두개 출력
          .groupBy(team.name)
          .fetch();
    
      Tuple teamA = result.get(0);
      Tuple teamB = result.get(1);
    
      assertThat(teamA.get(team.name)).isEqualTo("teamA");
      assertThat(teamA.get(member.age.avg())).isEqualTo(15); // (10 + 20) / 2
    
      assertThat(teamB.get(team.name)).isEqualTo("teamB");
      assertThat(teamB.get(member.age.avg())).isEqualTo(35); // (30 + 40) / 2
    }

    groupBy() , having() 예시

     ...
        .groupBy(item.price)
        .having(item.price.gt(1000))
        ...

    조인 - 기본 조인

    조인의 기본 문법은 첫번째 파라미터에 조인 대상을 지정하고, 두 번째 파라미터에 별칭(alias)으로 사용할 Q 타입을 지정하면 된다.

     

    예시 1 :

    조건 : 팀 A에 소속된 모든 회원을 찾기

    @Test
    public void join() {
      List<Member> result = queryFactory
          .selectFrom(member)
          // 여기서 team은 Qteam.team 을 말함.
          // innerJoin, leftJoin, rightJoin 으로도 사용 가능
          .join(member.team, team)
          .where(team.name.eq("teamA"))
          .fetch();
    
      assertThat(result)
          // extracting()을 사용해서 list에서 "username"만 추출
          .extracting("username")
          // containsExactly을 사용해서 순서를 포함해서 정확히 일치하는지 확인한다.
          .containsExactly("member1","member2");
    }

    예시 2 :

    조건: 회원의 이름이 팀 이름과 같은 회원을 조회

    /**
     * 세타 조인
     * 회원의 이름이 팀 이름과 같은 회원을 조회
     */
    @Test
    public void theta_join() {
      em.persist(new Member("teamA"));
      em.persist(new Member("teamB"));
      em.persist(new Member("teamC"));
    
      // 모든 멤버 테이블이랑 모든 팀 테이블이랑 전부 join을 한 뒤
      // join 한 테이블에서 멤버의 이름이랑 팀 이름이랑 같은 것을 찾아서 결과를 가져온다.
    
      List<Member> result = queryFactory
          .select(member)
          // 기존 join은 member와 연관관계에 있는 team을 찍은 다음에 team을 찍는다.
          // join(member.team, team)
          // 하지만 세타 조인은 그냥 나열한다.
          .from(member, team)
          .where(member.username.eq(team.name))
          .fetch();
    
      assertThat(result)
          .extracting("username")
          .containsExactly("teamA","teamB");
    }

    강의를 듣다가 테스트 코드를 수정할 일이 생겨서 테스트 코드를 수정했다.

    원래 initData에서 게시글 더미데이터를 사용했는데 이걸 사용하면 h2 콘솔에 게시글이 생성된다고 해서 게시글 더미데이터를 지운다 해서 테스트 코드를 전부 수정했다.

       // 게시글 작성
        QuestionBoardRequest questionBoardRequest = new QuestionBoardRequest("제목", "내용", 10,
            QuestionCategory.BACKEND);
        List<MultipartFile> multipartFiles = new ArrayList<>();
        MockMultipartFile multipartFile = new MockMultipartFile("files", "imageFile.jpeg", "image/jpeg",
            "<<jpeg data>>".getBytes());
        multipartFiles.add(multipartFile);
        QuestionBoard createQuestionBoard = questionBoardService.createQuestionBoard(
            questionBoardRequest, multipartFiles, findUser.get());
    
        List<BoardImage> originBoardImageList = boardImageRepository.findAllByBoardId(
            createQuestionBoard.getId());
        List<String> originImagePaths = new ArrayList<>();
        for (BoardImage boardImage : originBoardImageList) {
          originImagePaths.add(boardImage.getImagePath());
        }
    
        // 댓글 작성
        CommentRequest request = new CommentRequest("질문댓글");
    
        // when
        Comment comment = commentService.createQuestionComment(createQuestionBoard.getId(), request,
            findUser.get());
    
        // then
        assertThat(comment.getContent()).isEqualTo(request.getContent());
        // 자신이 만든 질문 게시글에 댓글을 달면 나오는 익셉션 테스트, 서비스에서 주석 풀면 정상 작동
    //    assertThrows(CustomException.class,
    //        () -> commentService.createQuestionComment(1L, request, findUser.get()));
      }

    원래 댓글을 생성할때 원래 더미데이터에 있는 게시글을 사용했는데(ex. 1L) 이제는 게시글을 직접 만들고 그 게시글의 id를 받도록 만들었다.

    // when
    // createQuestionBoard.getId()로 게시글의 아이디를 받아온다.
    Comment comment = commentService.createQuestionComment(createQuestionBoard.getId(), request,
        findUser.get());

    이런식으로 게시글, 대댓글, 좋아요까지 전부 수정했다.

    통합 테스트 시 전부 통과한다.

    내일 목표: Querydsl 강의 전부 듣기 (시간 남으면), 발표 자료 준비


    참고한 블로그들

    https://bcp0109.tistory.com/317

     

    JUnit 에서 AssertJ 로 contains 포함 여부 테스트

    1. Overview Java 에서 테스트 코드를 짤 때 특정 자료구조의 원소 값을 확인해야 하는 테스트가 있습니다. 반복문을 돌면서 일일히 확인해야 하거나 그냥 코드 한줄 한줄 입력하는 방법도 있지만 org

    bcp0109.tistory.com

    https://pjh3749.tistory.com/241

     

    [AssertJ] JUnit과 같이 쓰기 좋은 AssertJ 필수 부분 정리

    AssertJ가 core document를 새로운 github.io로 이전했네요 :) . 본 글은 AssertJ 공식 문서를 핵심 챕터를 선정하여 번역하며 정리한 글 입니다. http://joel-costigliola.github.io/assertj/assertj-core.html AssertJ란 무엇인

    pjh3749.tistory.com

     

Designed by Tistory.