[복습] Querydsl - 기본 문법

김주형·2024년 6월 25일
0

Web Basic

목록 보기
48/57
post-thumbnail

예제 도메인 모델

엔티티 클래스

ERD

Member 엔티티


@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
public class Member {
	@Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
    private int age;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team;
    
    public Member(String username) {
    	this(username, 0);
    }
    
    public Member(String username, int age) {
    	this(username, age, null);
    }
    
    public Member(String username, int age, Team team) {
    	this.username = username;
        this.age = age;
        if (team != null) {
        	changeTeam(team);
        }
     }
     
     private void changeTeam(Team team) {
     	this.team = team;
        team.getMembers().add(this);
     }
 }
  • @Setter : 실무에서 가급적 Setter는 사용하지 않습니다
  • @NoArgsConstructor AccessLevel.PROTECTED : 기본 생성자 막고 싶은데, JPA 스팩상 PROTECTED로 열어둡니다
  • @ToString은 가급적 내부 필드만(연관관계 없는 필드만)
  • changeTeam() 으로 양방향 연관관계 한번에 처리(연관관계 편의 메소드

Team 엔티티

// ..
  
@Entity  
@Getter @Setter  
@NoArgsConstructor(access = AccessLevel.PROTECTED)  
@ToString(of = {"id", "name"})  
public class Team {  
  
    @Id @GeneratedValue  
    private Long id;  
    private String name;  
  
    @OneToMany(mappedBy = "team")  
    private List<Member> members = new ArrayList<>();  
  
    public Team(String name) {  
        this.name = name;  
    }  
}
  • Member와 Team은 양방향 연관관계, Member.team이 연관관계의 주인, Team.members는 연관관계의 주인이 아닙니다
  • Member.team이 데이터베이스 외래키 값을 변경할 수 있습니다
  • Team.members은 연관관계의 주인이 아니므로, 조회만 가능합니다

MemberTest- 데이터 확인 테스트

@SpringBootTest  
@Transactional  
@Commit  
class MemberTest {  
  
    @PersistenceContext  
    EntityManager em;  
  
    @Test  
    public void testEntity() {  
        Team teamA = new Team("teamA");  
        Team teamB = new Team("teamB");  
        em.persist(teamA);  
        em.persist(teamB);  
  
        Member member1 = new Member("member1", 10, teamA);  
        Member member2 = new Member("member2", 20, teamA);  
        Member member3 = new Member("member3", 30, teamB);  
        Member member4 = new Member("member4", 40, teamB);  
  
        em.persist(member1);  
        em.persist(member2);  
        em.persist(member3);  
        em.persist(member4);  
  
        // 초기화  
        em.flush();  
        em.clear();  
  
        // 확인  
        List<Member> members = em.createQuery("select m from Member m", Member.class)  
                .getResultList();  
  
        for (Member member : members) {  
            System.out.println("member = " + member);  
            System.out.println("-> member.team" + member.getTeam());  
        }  
    }  
}
  • 가급적 순수 JPA로 동작 확인합니다
  • db 테이블 결과 확인합니다
  • 지연 로딩 동작을 확인합니다

실행 결과

시작 - JPQL vs Querydsl

테스트 기본 코드

@SpringBootTest  
@Transactional  
public class QuerydslBasicTest {  
  
    @Autowired  
    EntityManager em;  
  
    @BeforeEach  
    public void before() {  
        Team teamA = new Team("teamA");  
        Team teamB = new Team("teamB");  
        em.persist(teamA);  
        em.persist(teamB);  
  
        Member member1 = new Member("member1", 10, teamA);  
        Member member2 = new Member("member2", 20, teamA);  
        Member member3 = new Member("member3", 30, teamA);  
        Member member4 = new Member("member4", 40, teamA);  
  
        em.persist(member1);  
        em.persist(member2);  
        em.persist(member3);  
        em.persist(member4);  
    }  
  • 이제 이 예제로 실행합니다

Querydsl vs JPQL

@Test  
public void startJPQL() {  
    // member1를 찾아라  
    Member findMember = em.createQuery("select m from Member m where m.username = :username", Member.class)  
            .setParameter("username", "member1")  
            .getSingleResult();  
  
    assertThat(findMember.getUsername()).isEqualTo("member1");  
  
}  
  
@Test  
public void startQuerydsl() {  
	// member1을 찾아라
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);  
    QMember m = new QMember("m"); // 어떤 qmember인지 이름을 준 것이다.  
  
    Member findMember = queryFactory  
            .select(m)  
            .from(m)  
            .where(m.username.eq("member1"))  // 파라미터 바인딩 처리
            .fetchOne();  
  
    assertThat(findMember.getUsername()).isEqualTo("member1");  
}
  • EntityManager로 JPAQueryFactory 생성
  • Querydsl은 JPQL 빌더
  • JPQL : 문자 (실행 시점 오류) vs Querydsl : 코드 (컴파일 시점 오류)
  • JPQL : 파라미터 바인딩 직접 vs Querydsl : 파라미터 바인딩 자동 처리
  • Querydsl은 문법을 틀렸을 경우, 컴파일 타임에 오류를 바로 잡을 수 있습니다

JPAQueryFactory를 필드로

@SpringBootTest
@Transactional
public class QuerydslBasicTest {

    @PersistenceContext
    EntityManager em;
    
    JPAQueryFactory queryFactory;
    
    @BeforeEach
    public void before() {
        queryFactory = new JPAQueryFactory(em);
        //…
    }
    
    @Test
    public void startQuerydsl2() {
        //member1을 찾아라.
        QMember m = new QMember("m");
        Member findMember = queryFactory
            .select(m)
            .from(m)
            .where(m.username.eq("member1"))
            .fetchOne();
        assertThat(findMember.getUsername()).isEqualTo("member1");
    }
}
  • JPAQueryFactory를 필드로 제공하면 동시성 문제는 JPAQueryFactory를 생성할 때 제공하는EntityManager(em)에 달려있습니다

  • 스프링 프레임워크는 여러 쓰레드에서 동시에 같은 EntityManager에 접근해도, 트랜잭션 마다 별도의 영속성 컨텍스트를 제공하기 때문에, 동시성 문제는 걱정하지 않아도 된다고 합니다

실행 결과

기본 Q-Type 활용

기본 Q-Type 활용

  • Q클래스 인스턴스를 사용하는 2가지 방법
QMember qMember = new QMember("m"); // 별칭 직접 지정
QMember qMember = QMember.member; // 기본 인스턴스 사용

이전에는 Q클래스 인스턴스를 new를 사용하여 별칭을 직접 지정한 방법을 사용했는데, Querydsl이 기본적으로 제공하는 기본 인스턴스를 사용하는 것이 더 간편합니다

  • 기본 인스턴스를 static import와 함께 사용

동시성 문제를 걱정하지 않아도 된다고 합니다
이전에 실행되는 @Test before에서 queryFactory = new JPAQueryFactory(em);와 같이 구현하면 됩니다
구현할 시, 실행하기 전 @BeforeEach 애노테이션이 적힌 메서드가 실행됩니다

import static study.querydsl.entity.QMember.*;

@Test
public void startQuerydsl3() {
	//member1을 찾아라
    Member findMember = queryFactory
    		.select(member)
            .from(member)
            .where(member.username.eq("member1"))
            .fetchOne();
            
     assertThat(findMember.getUsername()).isEqualTo("member1");
}

이렇게 쓰는 방법을 권장합니다

  • static import를 사용하면 간편하게 사용할 수 있습니다

실행되는 JPQL 보기

위에 작성한 소스 실행시(member1 일때)

  • 첫번째가 JPQL, 두번째 Querydsl
@Test
public void startQuerydsl3(){
	// member1을 차자라
    // 같은 테이블을 조인하는 경우 이와 같이 사용합니다
    QMember m1 = new QMember("m1");
    Member findMember = queryFactory.select(m1)
    					.from(m1)
                        .where(m1.username.eq("member1))
                        .fetchOne();
                        
    assertThat(findMember.getUsername()).isEqualTo("member1");
}
  • 입력한 m1이 보입니다
  • 같은 테이블을 조인하는 경우 이와 같이 사용합니다

검색 조건 쿼리

  • 기본 검색 쿼리
@Test  
public void search() {  
    Member findMember = queryFactory  
            .selectFrom(member)  
            .where(member.username.eq("member1")  
                    .and(member.age.eq(10)))  
            .fetchOne();  
    assertThat(findMember.getUsername()).isEqualTo("member1");  
}
  • 검색 조건은 .and(), .or()를 메서드 체인으로 연결할 수 있습니다

실행 결과

참고
select, from을 selectFrom으로 합칠 수 있습니다

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%’ 검색 ...

AND 조건을 파라미터로 처리

@Test  
public void searchAndParam() {  
    List<Member> result1 = queryFactory  
            .selectFrom(member)  
            .where(  
                    member.username.eq("member1"),  
                    member.age.eq(10))  
            .fetch();  
  
    assertThat(result1.size()).isEqualTo(1);  
}
  • where()에 파라미터로 검색조건을 추가하면 AND 조건이 추가됩니다
  • 이 경우 null 값은 무시한다. → 메서드 추출을 활용해서 동적 쿼리를 깔끔하게 만들 수 있습니다

결과 조회

  • fetch(): 리스트 조회, 데이터 없으면 빈 리스트 반환
  • fetchOne(): 단건 조회
    • 결과가 없으면: null
    • 결과가 둘 이상이면: com.querydsl.core.NonUniqueResultException
  • fetchFirst(): limit(1).fetchOne()와 같다.
  • fetchResults(): 페이징 정보 포함, total count 쿼리 추가 실행
  • fetchCount(): count 쿼리로 변경해서 count 수 조회
//List
List<Member> fetch = queryFactory
        .selectFrom(member)
        .fetch();
        
//단건
Member findMember1 = queryFactory
        .selectFrom(member)
        .fetchOne();
        
//처음 한 건 조회
Member findMember2 = queryFactory
        .selectFrom(member)
        .fetchFirst();
        
//페이징에서 사용
QueryResults<Member> results = queryFactory
        .selectFrom(member)
        .fetchResults();
        
//count 쿼리로 변경
long count = queryFactory
        .selectFrom(member)
        .fetchCount();

페이징에서 사용

@Test  
    public void resultFetch() { 
		 
        QueryResults<Member> results = queryFactory  
                .selectFrom(member)  
                .fetchResults();  
  
        results.getTotal();  
        List<Member> content = results.getResults();

  • select 문 두번 실행됩니다

count 쿼리로 변경

@Test
public void resultFetch() {
	long total = queryFactory
    			.selectFrom(member)
                .fetchCount();

정렬

/**
 * 회원 정렬 순서
 * 1. 회원 나이 내림차순(desc)
 * 2. 회원 이름 올림차순(asc)
 * 단 2에서 회원 이름이 없으면 마지막에 출력(nulls last)
 */
@Test
public void sort() {
    em.persist(new Member(null, 100));
    em.persist(new Member("member5", 100));
    em.persist(new Member("member6", 100));
    
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.eq(100))
            .orderBy(member.age.desc(), member.username.asc().nullsLast())
            .fetch();
            
    Member member5 = result.get(0);
    Member member6 = result.get(1);
    Member memberNull = result.get(2);
    assertThat(member5.getUsername()).isEqualTo("member5");
    assertThat(member6.getUsername()).isEqualTo("member6");
    assertThat(memberNull.getUsername()).isNull();
}
  • desc(), asc() : 일반 정렬
    • desc() : 내림차순
    • asc() : 오름차순
  • nullsLast(), nullsFirst() : null 데이터 순서 부여

실행 결과

페이징

조회 건수 제한

@Test  
    public void paging1() {  
        List<Member> result = queryFactory  
                .selectFrom(member)  
                .orderBy(member.username.desc())  
                .offset(1)  
                .limit(2)  
                .fetch();  
  
        assertThat(result.size()).isEqualTo(2);  
    }  

전체 조회 수가 필요한 경우

@Test  
public void paging2() {  
    QueryResults<Member> queryResults = queryFactory  
            .selectFrom(member)  
            .orderBy(member.username.desc())  
            .offset(1)  
            .limit(2)  
            .fetchResults();  
  
    assertThat(queryResults.getTotal()).isEqualTo(4);  
    assertThat(queryResults.getLimit()).isEqualTo(2);  
    assertThat(queryResults.getOffset()).isEqualTo(1);  
    assertThat(queryResults.getResults().size()).isEqualTo(2);  
}


💡 참고

  • 실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리는 조인이 필요 없는 경우도 있습니다
  • 그런데 이렇게 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 해버리기 때문에 성능이 안나올 수 있습니다
  • count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성해야 합니다

집합

집합 함수

/**  
 * JPQL * select * COUNT(m), //회원수  
 * SUM(m.age), //나이 합  
 * AVG(m.age), //평균 나이  
 * MAX(m.age), //최대 나이  
 * MIN(m.age) //최소 나이 * from Member m  
 */@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();  
  
    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);  
}
  • JPQL이 제공하는 모든 집합 함수를 제공합니다
  • tuple은 프로젝션과 결과반환에서 공부합니다

실행 결과

GroupBy 사용

/**  
 * 팀의 이름과 각 팀의 평균 연령을 구해라.  
 */@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);  
}

groupBy를 사용하여 그룹별로 원하는 결과를 얻어낼 수 있습니다
그룹화된 결과 중 원하는 조건의 결과만 필터링하기 위해서 having을 사용할 수 있습니다

ex) groupBy(), having() 예시

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

: item의 price를 기준으로 그룹핑을 하되, 가격이 1000보다 큰 값만 그룹핑 합니다

실행 결과

조인 - 기본 조인

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

  • 첫번째 파라미터에 조인 대상을 지정합니다
  • 두번째 파라미터에 별칭(alias)로 사용할 Q타입을 지정하면 됩니다
/**  
 * 팀 A에 소속된 모든 회원  
 */  
@Test  
public void join() {  
	QMember member = QMember.member;
	QTeam team = QTeam.team;
	List<Member> result = queryFactory  
            .selectFrom(member)  
            .join(member.team, team)  
            .where(team.name.eq("teamA"))  
            .fetch();  
  
    assertThat(result)  
            .extracting("username")  
            .containsExactly("member1", "member2");  
}
  • join(), innerJoin(): 내부 조인
  • leftJoin(): left 외부 조인
  • rightJoin(): right 외부 조인
  • JPQL의 on과 성능 최적화를 위한 fetch 조인 제공

실행 결과

세타 조인

연관관계가 없는 필드로 조인

/**  
 * 세타 조인(연관관계가 없는 필드로 조인)  
 * 회원의 이름이 팀 이름과 같은 회원 조회  
 */  
@Test  
public void theta_join() {  
    em.persist(new Member("teamA"));  
    em.persist(new Member("teamB"));  
    em.persist(new Member("teamC"));  
  
    List<Member> result = queryFactory  
            .select(member)  
            .from(member, team)  
            .where(member.username.eq(team.name))  
            .fetch();  
  
    assertThat(result)  
            .extracting("username")  
            .containsExactly("teamA", "teamB");  
}
  • from 절에 여러 엔티티를 선택해서 세타 조인
  • 지금은 외부 조인 불가능 -> 조인 on을 사용하면 외부 조인 가능

실행 결과

조인 - on절

ON 절을 활용한 조인(JPA 2.1부터 지원)
1. 조인 대상 필터링
2. 연관관계 없는 엔티티 외부 조인

조인 대상 필터링

예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회

/**  
 * 예) 회원과 팀을 조인하면서, 팀 이름이 teamA인 팀만 조인, 회원은 모두 조회  
 * 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() {  
    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);  
    }  
}

실행 결과

참고

  • on 절을 활용해 조인 대상을 필터링 할 때, 외부조인이 아니라 내부조인(inner join)을 사용하면, where 절에서 필터링 하는 것과 기능이 동일합니다
  • 따라서 on 절을 활용한 조인 대상 필터링을 사용할 때, 내부조인 이면 익숙한 where 절로 해결하고, 정말 외부조인이 필요한 경우에만 이 기능을 사용 습관화를 지향합니다

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

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

/**  
 * 2. 연관관계 없는 엔티티 외부 조인  
 * 예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인  
 */  
@Test  
public void join_on_no_filtering() {  
    em.persist(new Member("teamA"));  
    em.persist(new Member("teamB"));  
    em.persist(new Member("teamC"));  
  
    List<Tuple> result = queryFactory  
            .select(member, team)  
            .from(member)  
            .leftJoin(team).on(member.username.eq(team.name))  
            .fetch();  
  
    for (Tuple tuple : result) {  
        System.out.println("tuple = " + tuple);  
    }  
}
  • 하이버네이트 5.1부터 on절 을 사용해서 서로 관계가 없는 필드로 외부 조인하는 기능이 추가되었습니다
    • 물론 내부 조인도 가능합니다

문법을 주의
leftJoin() 부분에 일반 조인과 다르게 엔티티 하나만 들어갑니다

  • 일반조인: leftJoin(member.team, team)
  • on조인: from(member).leftJoin(team).on(xxx)

실행 결과

조인 - 페치 조인

  • 페치 조인은 SQL에서 제공하는 기능은 아닙니다
  • SQL 조인을 활용해서 연관된 엔티티를 SQL 한번에 조회하는 기능입니다
  • 주로 성능 최적화에 사용하는 방법입니다

페치 조인 미적용

지연로딩으로 Member, Team SQL 쿼리 각각 실행

@PersistenceUnit  
EntityManagerFactory emf;  
  
@Test  
public void fetchJoinNo() throws Exception {  
    em.flush();  
    em.clear();  
  
    Member findMember = queryFactory  
            .selectFrom(member)  
            .where(member.username.eq("member1"))  
            .fetchOne();  
  
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());  
    assertThat(loaded).as("페치 조인 미적용").isFalse();  
}
  • Member 엔티티만 조회합니다. 연관관계에 있는 Team은 조회하지 않습니다

실행 결과

페치 조인 적용

즉시로딩으로 Member, Team SQL 쿼리 조인으로 한번에 조회

@Test  
public void fetchJoinUse() throws Exception {  
    em.flush();  
    em.clear();  
  
    Member findMember = queryFactory  
            .selectFrom(member)  
            .join(member.team, team).fetchJoin()  
            .where(member.username.eq("member1"))  
            .fetchOne();  
  
    boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam());  
    assertThat(loaded).as("페치 조인 적용").isTrue();  
}
  • join(), leftJoin() 등 조인 기능 뒤에 fetchJoin() 이라고 추가하면 됩니다

실행 결과

서브 쿼리

com.querydsl.jpa.JPAExpressions 사용

서브 쿼리 eq 사용

/**
 * 나이가 가장 많은 회원 조회
 */
@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);
}

서브 쿼리 goe 사용

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();
        
assertThat(result).extracting("age")
        .containsExactly(30,40);

}


![](https://velog.velcdn.com/images/urtimeislimited/post/6e147b22-8d87-46e5-9731-17842825fbdb/image.png)

서브쿼리 여러건 처리 in 사용
```java
/**
 * 서브쿼리 여러 건 처리, in 사용
 */
@Test
public void subQueryIn() throws Exception {

    QMember memberSub = new QMember("memberSub");
    
    List<Member> result = queryFactory
            .selectFrom(member)
            .where(member.age.in(
                    JPAExpressions
                            .select(memberSub.age)
                            .from(memberSub)
                            .where(memberSub.age.gt(10))
            ))
            .fetch();
            
    assertThat(result).extracting("age")
            .containsExactly(20, 30, 40);
}

select 절에 subquery

@Test  
public void selectSubQuery() {  
    QMember memberSub = new QMember("memberSub");  
      
    List<Tuple> result = queryFactory  
            .select(member.username,  
                    JPAExpressions  
                            .select(memberSub.age.avg())  
                            .from(memberSub)  
            ).from(member)  
            .fetch();  
  
    for (Tuple tuple : result) {  
        System.out.println("username = " + tuple.get(member.username));  
        System.out.println("age = " + tuple.get(JPAExpressions.select(memberSub.age.avg()).from(memberSub)));  
    }  
}


static import 활용

import static com.querydsl.jpa.JPAExpressions.select;

@Test  
public void selectSubQuery() {  
    QMember memberSub = new QMember("memberSub");  
  
    List<Tuple> result = queryFactory  
            .select(member.username,  
                    select(memberSub.age.avg())  
                            .from(memberSub))  
            .from(member)  
            .fetch();  
  
    for (Tuple tuple : result) {  
        System.out.println("username = " + tuple.get(member.username));  
        System.out.println("age = " + tuple.get(JPAExpressions.select(memberSub.age.avg()).from(memberSub)));  
    }  
  
  
}

from절의 서브쿼리 한계

  • JPA JPQL 서브쿼리의 한계점으로 from 절의 서브쿼리(인라인 뷰)는 지원하지 않습니다
  • Querydsl도 지원하지 않습니다
  • 하이버네이트 구현체를 사용하면 select 절의 서브쿼리는 지원합니다
  • Querydsl도 하이버네이트 구현체를 사용하면 select 절의 서브쿼리를 지원합니다

from 절의 서브쿼리 해결방안
1. 서브쿼리를 join으로 변경합니다 → 가능할 때도, 불가능할 때도 있습니다
2. 애플리케이션에서 쿼리를 2번 분리해서 실행합니다
3. nativeSQL을 사용합니다

Case 문

select, 조건절(where), order by에서 사용 가능
단순한 조건

@Test  
public void basicCase() {  
    List<String> result = queryFactory  
            .select(member.age  
                    .when(10).then("열살")  
                    .when(20).then("스무살")  
                    .otherwise("기타"))  
            .from(member)  
            .fetch();  
  
    for (String s : result) {  
        System.out.println("s = " + s);  
    }  
}

age가

  • 10이면 열살 출력
  • 20이면 스무살 출력
  • 그 밖에는 기타 출력

실행 결과

복잡한 조건

@Test  
public void complexCase() {  
    List<String> result = queryFactory  
            .select(new CaseBuilder()  
                    .when(member.age.between(0, 20)).then("0~20살")  
                    .when(member.age.between(21, 30)).then("21~30살")  
                    .otherwise("기타"))  
            .from(member)  
            .fetch();  
  
    for (String s : result) {  
        System.out.println("s = " + s);  
    }  
}
  • CaseBuilder()를 통해 동작합니다
  • when 절 안에 조건이 들어갑니다

실행 결과

orderBy에서 Case문 함께 사용하기 예제
예를 들어서 다음과 같은 임의의 순서로 회원을 출력하고 싶다면?
1. 0 ~ 30살이 아닌 회원을 가장 먼저 출력
2. 0 ~ 20살 회원 출력
3. 21 ~ 30살 회원 출력

@Test  
public void exampleCase(){  
    NumberExpression<Integer> rankPath = new CaseBuilder()  
            .when(member.age.between(0, 20)).then(2)  
            .when(member.age.between(21, 30)).then(1)  
            .otherwise(3);  
  
    List<Tuple> result = queryFactory  
            .select(member.username, member.age, rankPath)  
            .from(member)  
            .orderBy(rankPath.desc())  
            .fetch();  
  
    for (Tuple tuple : result) {  
        String username = tuple.get(member.username);  
        Integer age = tuple.get(member.age);  
        Integer rank = tuple.get(rankPath);  
        System.out.println("username = " + username + " age = " + age + " rank = " + rank);  
    }  
}
  • Querydsl은 자바 코드로 작성하기 때문에 rankPath 처럼 복잡한 조건을 변수로 선언해서 select절, orderBy 절에서 함께 사용할 수 있습니다

실행 결과

상수, 문자 더하기

상수가 필요하면 Expressions.constant(xxx) 사용합니다

@Test  
public void constant() {  
    List<Tuple> result = queryFactory  
            .select(member.username, Expressions.constant("A"))  
            .from(member)  
            .fetch();  
  
    for (Tuple tuple : result) {  
        System.out.println("tuple = " + tuple);  
    }  
}

이와 같이 최적화가 가능하면 SQL에 constant 값을 넘기지 않습니다
상수를 더하는 것처럼 최적화가 어려우면 SQL에 constant 값을 넘깁니다

실행 결과

문자 더하기 concat

@Test  
public void concat() {  
    List<String> result = queryFactory  
            .select(member.username.concat("_").concat(member.age.stringValue()))  
            .from(member)  
            .where(member.username.eq("member1"))  
            .fetch();  
  
    for (String s : result) {  
        System.out.println("s = " + s);  
    }  
}
  • member.age.stringValue() 부분이 중요합니다
  • 문자가 아닌 다른 타입들은 stringValue()로 문자로 변환할 수 있습니다
  • 이 방법은 ENUM을 처리할 때도 매우 자주 사용합니다

실행 결과

profile
도광양회

0개의 댓글