프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있다 

대상이 둘 이상이면 튜플이나 dto로 조회해야한다 

 

프로젝션 대상이 둘 이상일 때 튜플과 dto로 반환하는 방법이 있다. 

튜플로 조회하는 방법

List<Tuple> result = queryFactory
        .select(member.username, member.age)
        .from(member)
        .fetch();

dto로 반환하는 방법

1. 순수 JPA에서 DTO 조회하는 방법(비추천)

비추천하는 이유 

new 명령어를 사용하고, dto가 있는 패키지 명을 다 명시해야해서 불편하다

생성자 방식만 지원한다 

List<MemberDto> result = em.createQuery(
        "select new study.querydsl.dto.MemberDto(m.username, m.age) " +
                "from Member m", MemberDto.class)
        .getResultList();

 

2. Querydsl 빈 생성(bean population)

에서 3가지 방법을 지원한다(프로퍼티 접근, 필드 직접 접근, 생성자 사용)

1) 프로퍼티 접근(setter)

Projections.bean(Dto 클래스, 호출할 필드) 형식으로 작성 

List<MemberDto> result = queryFactory
        .select(Projections.bean(MemberDto.class,
                member.username,
                member.age))
        .from(member)
        .fetch();

 

2) 필드 직접 접근

Projections.fields(Dto 클래스, 호출할 필드) 형식으로 작성 

 List<MemberDto> result = queryFactory
         .select(Projections.fields(MemberDto.class,
                 member.username,
                 member.age))
         .from(member)
         .fetch();

프로퍼티나,필드 접근 방식에서 필드 이름이 다를 때(별칭이 다를 때) 해결방안

예시)

1. Member(엔티티)에서는 사용자 필드 이름이 username인데, UserDto에서는 사용자 필드 이름이 name일 때

member.username.as("name")으로 필드에 별칭을 적용한다 

.select(Projections.fields(UserDto,Class,
                member.username.as("name"),

2. 필드나, 서브쿼리에 별칭을 적용할 때는 ExpressionUtils.as(source,alias) 사용 

source: 나이가 가장 많은 회원을 조회하는 서브쿼리 

alias: "age"

ExpressionUtils.as(
        JPAExpressions
        .select(memberSub.age.max())
		.from(memberSUb),"age")

 

-전체 코드 

List<UserDto> fetch = queryFactory
		.select(Projections.fields(UserDto,Class,
                member.username.as("name"),
                ExpressionUtils.as(
                         JPAExpressions
                                .select(memberSub.age.max())
                                .from(memberSUb),"age")
                )
         ).from(member)
         .fetch();

 

3) 생성자 사용 

Projections.constructor()를 사용한다 

 List<MemberDto> result = queryFactory
        .select(Projections.constructor(MemberDto.class,
}
        member.username,
        member.age))
.from(member)
.fetch();

Querydsl에서 서브쿼리는 JPAExpressions를 이용한다 

서브쿼리에 사용하는 QType은 별도로 지정한 후 사용해야한다 

1. 나이가 가장 많은 회원 조회하기: max()함수 이용 

@Test
public void subQuery() throws Exception {

    QMember memberSub = new QMember("memberSub");
    
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(
                 //회원 중 가장 많은 나이를 조회하는 서브쿼리 
                    JPAExpressions
                            .select(memberSub.age.max())
                            .from(memberSub)
)) 
.fetch();

      assertThat(result).extracting("age")
              .containsExactly(40);
}

2. 나이가 평균 나이 이상인 회원: goe(이상) 사용

@Test
public void subQueryGoe() throws Exception {

    QMember memberSub = new QMember("memberSub");
    
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.goe(
                    JPAExpressions
                            .select(memberSub.age.avg())
                            .from(memberSub)
)) 
.fetch();
      
      ssertThat(result).extracting("age")
              .containsExactly(30,40);
}

3. 서브쿼리 여러 건을 처리하기 위해 In을 사용한다 

.where(member.age.in(
                      JPAExpressions
                              .select(memberSub.age)
                              .from(memberSub)
                              .where(memberSub.age.gt(10))

4. select 절에 subquery

List<Tuple> fetch = queryFactory
        .select(member.username,
                JPAExpressions
                        .select(memberSub.age.avg())
                        .from(memberSub)
        ).from(member)
        .fetch();
        
for (Tuple tuple : fetch) {

    System.out.println("username = " + tuple.get(member.username));
    System.out.println("age = " + tuple
    		.get(JPAExpressions
    		.select(memberSub.age.avg())
       .from(memberSub)));
}

 

JPA JPQL에서 from절에서의 서브쿼리를 지원하지 않으므로 Querydsl도 지원하지 않는다. 

 

그래서 from 절의 서브쿼리 해결방안으로는 

 

1. 서브쿼리를 join으로 변경한다.(가능한 상황도 있고, 불가능한 상황도 있다)

2. 애플리케이션에서 쿼리를 2번 분리해서 실행한다. SQL anti pattern 책 소개하심

3. nativeSQL을 사용한다 

기본 조인

-조인의 기본 문법

join(조인 대상, 별칭으로 사용할 Q타입)

teamA에 속하는 회원 조회(여기서는 member1,member2이다)

//팀A에 소속된 모든 회원 
@Test
public void join() throws Exception{
	
    QMember member= QMember.member;
    QTeam team= QTeam.team;
    
    List<Member> result= queryFactory
    	.selectFrom(member)
        .join(member.team,team)//조인대상, 별칭으로 사용할 QType
        .where(team.name.eq("teamA"))
        .fetch();
        
    assertThat(result)
    	.extracting("username")
        .containsExactly("member1","member2")
}

기본 조인 쿼리

/* select
        member1 
    from
        Member member1   
    left join
        member1.team as team 
    where //select 서브쿼리가 있다
        team.name = ?1 */ select 
            member0_.member_id as member_i1_1_,
            member0_.age as age2_1_,
            member0_.team_id as team_id4_1_,
            member0_.username as username3_1_ 
        from
            member member0_ 
        left outer join
            team team1_ 
                on member0_.team_id=team1_.team_id 
        where
            team1_.name=?

 

세타 조인

연관관계가 없는 필드로 조인하는 것 -> from 절에 여러 엔티티를 선택한다 (from member, team)

이 조인은 외부 조인(outer join)이 불가능하다

내부 조인(inner join)만 가능 

 

예제: 회원이름이 팀 이름과 같은 회원을 조회하려고 한다 

 @Test
public void theta_join() throws Exception {

    em.persist(new Member("teamA"));
    em.persist(new Member("teamB"));
    
    List<Member> result = queryFactory
            .select(member)
            .from(member, team)
            .where(member.username.eq(team.name))
            .fetch();
            
    assertThat(result)
            .extracting("username")
            .containsExactly("teamA", "teamB");
}

세타 조인 쿼리

/* select
        member1 
    from
        Member member1,
        Team team 
    where
        member1.username = team.name */ select
            member0_.member_id as member_i1_1_,
            member0_.age as age2_1_,
            member0_.team_id as team_id4_1_,
            member0_.username as username3_1_ 
        from
            member member0_ cross 
        join
            team team1_ 
        where
            member0_.username=team1_.name


조인- On절 

on절을 활용한 조인에는 1) 조인 대상 필터링 2) 연관관계 없는 엔티티 외부 조인 이 있다 

1. 조인 대상 필터링 

예시) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인하고, 회원은 모두 조회한다 -> leftJoin 이용

JPQL: SELECT m,t  FROM Member m LEFT JOIN m.team t on t.name = 'teamA'

SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t on m.TEAM_ID =t.id and t.name='teamA'

@Test
public void join_on_filtering() throws Exception {
	
	//모든 멤버가 다 조회되고, 팀 이름이 teamA가 아니면 null 처리된다 
    List<Tuple> result = queryFactory
            .select(member, team)
            .from(member)
            .leftJoin(member.team, team).on(team.name.eq("teamA"))
            .fetch();
            
    for (Tuple tuple : result) {
        System.out.println("tuple = " + tuple);
 } 
}

쿼리 결과 

t=[Member(id=3, username=member1, age=10), Team(id=1, name=teamA)]
t=[Member(id=4, username=member2, age=20), Team(id=1, name=teamA)]
t=[Member(id=5, username=member3, age=30), null]
t=[Member(id=6, username=member4, age=40), null]

 

참고: Inner join에 on 절을 사용하면 where 절에서 필터링 하는 것과 기능이 동일하다. 

따라서 inner join +on 절이면 익숙한 where 절로 해결하고, 외부조인이 필요한 경우에만 on절을 사용하자 

2. 연관관계 없는 엔티티 외부 조인 

예시) 회원의 이름과 팀의 이름이 같은 대상을 외부조인한다 

JPQL: SELECT m, t FROM Member m LEFT JOIN Team t ON m.username = : t.name

SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name

@Test
  public void join_on_no_relation() throws Exception {
  
      em.persist(new Member("teamA"));
      em.persist(new Member("teamB"));
      
      List<Tuple> result = queryFactory
              .select(member, team)
              .from(member)
              .leftJoin(team) //일반 leftJoin에 엔티티 하나만 들어간다
              .on(member.username.eq(team.name))
              .fetch();
              
      for (Tuple tuple : result) {
          System.out.println("t=" + tuple);
  } 
}

연관관계에서의 leftJoin은 leftJoin(member.team, team) 이지만, 

연관관계가 없는 곳에서 leftJoin은 leftJoin(team)으로 엔티티 하나만 들어간다 

쿼리 결과 

사용자 이름이 teamA,teamB인 경우만 팀 이름이 출력되었다

 t=[Member(id=3, username=member1, age=10), null]
  t=[Member(id=4, username=member2, age=20), null]
  t=[Member(id=5, username=member3, age=30), null]
  t=[Member(id=6, username=member4, age=40), null]
  t=[Member(id=7, username=teamA, age=0), Team(id=1, name=teamA)]
  t=[Member(id=8, username=teamB, age=0), Team(id=2, name=teamB)]

페치조인

@Test
public void fetchJoinUse() throws Exception {

    em.flush();
    em.clear();
    Member findMember = queryFactory
            .selectFrom(member)
            .join(member.team, team).fetchJoin() //일반 join에 .fetchJoin() 추가해주면된다
            .where(member.username.eq("member1"))
            .fetchOne();
            
    boolean loaded =
emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());

  assertThat(loaded).as("페치 조인 적용").isTrue(); 
}

집합 함수

여러 필드의 집합함수를 이용해 조회할 경우에는 반환타입이 List<Tuple>이 된다

/**
     * JPQL
     * select
* COUNT(m), //회원수
* SUM(m.age), //나이 합
* AVG(m.age), //평균 나이
* MAX(m.age), //최대 나이
* MIN(m.age) //최소 나이 * from Member m
*/
@Test
public void aggregation() throws Exception {
    List<Tuple> result = queryFactory
           .select(member.count(),
                    member.age.sum(),
                    member.age.avg(),
                    member.age.max(),
                    member.age.min())
            .from(member)
            .fetch();
            
    Tuple tuple = result.get(0);
    assertThat(tuple.get(member.count())).isEqualTo(4);
    assertThat(tuple.get(member.age.sum())).isEqualTo(100);
    assertThat(tuple.get(member.age.avg())).isEqualTo(25);
    assertThat(tuple.get(member.age.max())).isEqualTo(40);
    assertThat(tuple.get(member.age.min())).isEqualTo(10);
  }

GroupBy 사용

그룹화된 결과를 제한하려면 having 

**
* 팀의 이름과 각 팀 회원의 평균 연령을 구해라.
*/
@Test
public void group() throws Exception {

    List<Tuple> result = queryFactory
            .select(team.name, member.age.avg())
            .from(member)
            .join(member.team, team)
            .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);
    assertThat(teamB.get(team.name)).isEqualTo("teamB");
    assertThat(teamB.get(member.age.avg())).isEqualTo(35);

}

//having 사용하면
.groupBy(item.price)
.having(item.price.gt(1000)) //가격 1000원이상인 아이템을 조회
...

+ Recent posts