Study/실전! QueryDSL
Day 2. QueryDSL 기본 문법
주지민
2021. 9. 24. 13:02
반응형
본 포스트는 인프런 - 실전! QueryDSL의 강의를 듣고 공부한 내용입니다
https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84
1. 예제 도메인 모델
- 해당 포스트를 진행하면서 사용할 도메인 모델부터 정의해보려고 합니다
- 엔티티 ERD
- Member Entity Code
// Member.class @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @ToString(of = {"id", "userName", "age"}) @Entity 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, null); } 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 (Objects.nonNull(team)) { changeTeam(team); } } private void changeTeam(Team team) { this.team = team; team.getMembers().add(this); } }
- Team Entity Code
// Team.class @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter @ToString(of = {"id", "name"}) @Entity 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; } }
2. JPQL vs QueryDSL
- 간단한 테스트 코드를 통해 JPQL과 QueryDSL의 차이점을 알아보겠습니다
- JPQL Test Code
// JPQL Test Code @Test void startJPQL() { // given String expected = "memberA"; String qlString = "SELECT m FROM Member m WHERE m.userName = :userName"; // when Member actual = em.createQuery(qlString, Member.class) .setParameter("userName", expected) .getSingleResult(); // then assertThat(actual).isNotNull(); assertThat(actual.getUserName()).isEqualTo(expected); }
- QueryDSL Test Code
// Q Type Class compile ./gradlew compileQuerydsl
// QueryDSL Test Code @Test void startQueryDsl() { QMember m = new QMember("m"); Member actual = jpaQueryFactory .select(m) .from(m) .where(m.userName.eq("memberA")) .fetchOne(); assertThat(actual).isNotNull(); assertThat(actual.getUserName()).isEqualTo("memberA"); }
- 차이점
- JPQL과 다르게 문자열을 사용하지 않고 자바 메서드 호출로 쿼리를 만들어냄
- JPQL은 문자열 쿼리로 만들어내기 때문에 런타임 시점에서 쿼리 오류가 잡힌다 ( IDE의 도움을 받을 순 있음 )
- QueryDSL은 자바 메서드 호출로 쿼리를 만들어 내기 때문에 컴파일 시점에서 문법적 오류가 잡힌다
- JPQL처럼 쿼리에 ":{parameterName}"을 바인딩하지 않아도 만들어낸 Q Type 클래스의 eq, gt등의 메서드를 이용하여 바인딩된 쿼리를 만들어 낼 수 있다
- QueryDSL은 기본적으로 파라미터를 ?로 바인딩, prepareStatement의 파라미터 바인딩 방식을 사용
- JPQL과 다르게 문자열을 사용하지 않고 자바 메서드 호출로 쿼리를 만들어냄
3. Q-Type 클래스 활용
- Q-Type 클래스를 사용하는 방법 ( 생성하는 방법 )
- 생성자에 variable 파라미터(별칭)을 넘겨서 사용
// using variable QMember m = new QMember("m");
- 별칭은 말그래도 쿼리에서 사용하는 별칭이다 ( 같은 테이블을 조인해야하는 경우에만 별칭을 바꿔주면 된다 )
- 내부에 만들어둔 static instance를 이용
// Using static instance QMember m = QMember.member;
- 생성자에 variable 파라미터(별칭)을 넘겨서 사용
- 특히나 static instance를 이용할 경우 코드를 더 간결하게 유지할 수 있다 ( static import 이용 )
- 참고로 변환된 JPQL 쿼리를 보고 싶은 경우 jpa.properties.hibernate.use_sql_comments: true 속성을 추가해주도록 하자
4. 기본 문법 소개
4-1. 검색 조건 쿼리
- 기본 테스트 코드 예제
// test @Test void searchTest() { // when Member findMember = jpaQueryFactory .selectFrom(member) .where(member.userName.eq("memberA") .and(member.age.eq(10))) .fetchOne(); // then assertThat(findMember.getUserName()).isEqualTo("memberA"); assertThat(findMember.getAge()).isEqualTo(10); }
- select ... from ... 은 대상이 같을 경우 selectFrom으로 합칠 수 있다
- {Q-Type Class}.{속성}.{키워드} 형태로 where 조건절을 구성할 수 있으며 키워드에는 eq, between 등 다양한 기능을 제공
- 또한 각 속성들을 and, or 등으로 묶을 수도 있다 ( 쿼리 형태랑 비슷 )
- 키워드 종류 ( JPQL에서 지원하는 모든 것들을 메서드로 제공 )
키워드 설명 코드 예제 쿼리 eq 같다 member.username.eq("...") username = "..." ne 다르다 member.username.ne("...") username != "..." eq("...").not member.username.eq("...").not() isNotNull NULL이 아니다 member.username.isNotNull() username IS NOT NULL in 그룹셋 안에 포함되는지 member.username.in(...) username IN (...) not in 그룹셋 안에 포함되지 않는지 member.username.notIn(...) username NOT IN (...) between 범위안에 포함되는지 member.age.between(10, 20) age between 10 AND 20 goe 크거나 같은지 member.age.goe(10) age >= 10 gt 큰지 member.age.gt(10) age > 10 loe 작거나 같은지 member.age.loe(10) age <= 10 lt 작은지 member.age.lt(10) age < 10 like 해당 문자열과 같은지
* 패턴 일치 문자 사용 가능member.username.like("member%") username LIKE "member%" contains 해당 문자열이 포함되는지 member.username.contains("member") username LIKE "%member%" startsWith 해당 문자열로 시작하는지 member.username
.startsWith("member")username LIKE "member%" 기타 등등 - AND 연산은 직관적으로 and method 체인을 걸 수도 있지만, where 메서드에 파라미터로 넣어도 각 파라미터가 and로 연결된다
// 위 아래는 같은 결과 where(member.username.eq("memberA").and(member.age.eq(10)); where(member.username.eq("memberA"), member.age.eq(10));
4-2. 결과 조회
- fetch()
- 리스트 조회, 데이터 없으면 빈 리스트 반환
- Code
// fetch method @Test void fetchTest() { // when List<Member> members = jpaQueryFactory .selectFrom(member) .fetch(); // then assertThat(members).isNotEmpty(); assertThat(members).hasSize(4); }
- fetchOne()
- 단건 조회
- 결과가 없으면 NULL
- 결과가 두 개 이상이면 com.querydsl.core.NonUniqueResultException 발생
- Code
// fetchOne @Test void fetchOneTest() { // when Team actual = jpaQueryFactory .selectFrom(team) .where(team.name.eq("teamA")) .fetchOne(); // then assertThat(actual).isNotNull(); assertThat(actual.getName()).isEqualTo("teamA"); }
- fetchFirst()
- limit(1).fetchOne()
- 실제 limit 1 조건절을 추가하여 조회한다
- Code
# JPQL /* select member1 from Member member1 */ # SQL Query select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.user_name as user_nam3_1_ from member member0_ limit 1;
- fetchResults()
- 페이징(limit, offset 등) 관련 결과 조회
- 복잡한 쿼리를 fetchResults로 조회하게 되면 TotalCount를 조회하는 쿼리와 Contents를 조회하는 쿼리가 예상한 것과 상이할 수 있다, 이럴땐 fetchResults가 아닌 명시적으로 따로 두 개의 QueryDSL로 나눠 조회하자
- Code
// fetchResults @Test void fetchResultsTest() { // when QueryResults<Member> actual = jpaQueryFactory .selectFrom(member) .offset(2) .limit(2) .fetchResults(); // then assertThat(actual.getTotal()).isEqualTo(4); assertThat(actual.getResults().size()).isEqualTo(2); }
- total, size, limit, offset 등의 페이징 관련 속성들을 얻을 수 있다
- 조회 쿼리에 추가적으로 Count 쿼리가 한번 더 나가게 된다
- Query
// JPQL /* select count(member1) from Member member1 */ // SQL Query ( total count 조회 ) select count(member0_.member_id) as col_0_0_ from member member0_; // JPQL /* select member1 from Member member1 */ // SQL Query ( 실제 데이터 페이징 조회 ) select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.user_name as user_nam3_1_ from member member0_ limit 2, 2;
- fetchCount()
- count 쿼리로 변환하여 조회되는 결과 수만 반환
- Code
// fetchCount @Test void fetchCountTest() { // when long count = jpaQueryFactory .selectFrom(member) .fetchCount(); // then assertThat(count).isEqualTo(4l); }
- Query
// JPQL /* select count(member1)from Member member1 */ // SQL Query select count(member0_.member_id) as col_0_0_ from member member0_;
4-3. 정렬
- orderBy 메서드를 통해서 다양하게 정렬을 시킬 수 있다
- Code
// 예제 코드 @Test void sortTest() { // given em.persist(new Member(null, 100)); em.persist(new Member("member5", 100)); em.persist(new Member("member6", 100)); // when List<Member> members = jpaQueryFactory .selectFrom(member) .where(member.age.eq(100)) .orderBy(member.age.desc(), member.userName.asc().nullsLast()) .fetch(); // then assertThat(members).hasSize(3); assertThat(members) .extracting("userName") .containsSequence("member5", "member6", null); }
- when 부분을 보면 orderBy를 이용해 member의 "age" 속성은 내림차순(큰것부터 작은것으로)으로 "userName" 속성은 오름차순(작은것부터 큰것으로) 조건을 줬다
- 추가적으로 nullsLast()가 있는 것을 확인할 수 있는데 CASE문을 이용하여 값이 null일 경우의 추가적인 조건을 명시할 수 있다
- nullsLast(): null인 경우 제일 마지막
- nullsFirst(): null인 경우 제일 첫번째
- Query
// JPQL /* select member1 from Member member1 where member1.age = 100 order by member1.age desc, member1.userName asc nulls last */ // SQL Query select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.user_name as user_nam3_1_ from member member0_ where member0_.age=100 order by member0_.age desc, case when member0_.user_name is null then 1 else 0 end, member0_.user_name asc;
- order by member0.age desc, member0.user_name asc 를 확인할 수 있다
- nullsLast()에 해당하는 부분이 case ... end로 묶여 user_name 속성이 null일 경우 예외 조건을 만들어냈다
4-4. 페이징
- 페이징 쿼리를 만드는 방법
- offset ... limit ... 메서드를 이용하여 명시적으로 조회 ( count는 따로 조회해야 한다 )
- fetchResults를 이용하여 QueryDSL 자체적으로 페이징 조회 ( count까지 포함 )
- Code ( totalCount와 실제 Contents 따로 조회 )
// 페이징 테스트 @Test void pagingTest() { // when long totalCount = jpaQueryFactory .selectFrom(member) .fetchCount(); List<Member> members = jpaQueryFactory .selectFrom(member) .orderBy(member.userName.desc()) .offset(1) // 얼마나 스킵할 것인지 .limit(2) // 몇개나 가져올 것인지 .fetch(); // then assertThat(totalCount).isEqualTo(4); assertThat(members).hasSize(2); assertThat(members) .extracting("userName") .containsSequence("memberC", "memberB"); }
- Query ( totalCount와 실제 Contents 따로 조회 )
# JPQL /* select count(member1) from Member member1 */ # Query select count(member0_.member_id) as col_0_0_ from member member0_; # JPQL /* select member1 from Member member1 order by member1.userName desc */ # Query select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.user_name as user_nam3_1_ from member member0_ order by member0_.user_name desc limit 1, 2;
- Code ( fetchResults 이용 )
// 페이징 조회 ( fetchResults 이용 ) @Test void pagingTestWithFetchResults() { // when QueryResults<Member> queryResults = jpaQueryFactory .selectFrom(member) .orderBy(member.userName.desc()) .offset(1) .limit(2) .fetchResults(); // then assertThat(queryResults.getTotal()).isEqualTo(4); assertThat(queryResults.getResults()) .hasSize(2) .extracting("userName") .containsSequence("memberC", "memberB"); }
- 실제 발생하는 쿼리는 앞서 살펴본 totalCount와 실제 Contents 따로 조회했을 때 쿼리와 같다
4-5. 집합
- GroupBy ... , Having ... 을 이용한 그룹핑과 각종 집합 함수를 제공합니다
- Code
// aggregation test @Test void aggregationTest() { // when List<Tuple> list = jpaQueryFactory .select( member.count(), member.age.sum(), member.age.avg(), member.age.max(), member.age.min() ) .from(member) .fetch(); // then Tuple actual = list.get(0); assertThat(actual.get(member.count())).isEqualTo(4); assertThat(actual.get(member.age.sum())).isEqualTo(60); assertThat(actual.get(member.age.avg())).isEqualTo(15); assertThat(actual.get(member.age.max())).isEqualTo(20); assertThat(actual.get(member.age.min())).isEqualTo(10); }
- member.count(), member.age.avg() 등 각 리턴타입이 다르기 때문에 반환 결과는 com.querydsl.core 패키지의 Tuple 객체가 된다
- Query
# JPQL /* select count(member1), sum(member1.age), avg(member1.age), max(member1.age), min(member1.age) from Member member1 */ # SQL Query select count(member0_.member_id) as col_0_0_, sum(member0_.age) as col_1_0_, avg(member0_.age) as col_2_0_, max(member0_.age) as col_3_0_, min(member0_.age) as col_4_0_ from member member0_
4-5-1. 집합 ( GroupBy ... Having 예제 )
- GroupBy를 통해 대상을 묶을수도, 묶여진 대상들에 대해 Having으로 다시 제한을 걸수도 있다
- Code
// GroupBy ... Having ... @Test void groupByTest() { // when List<Tuple> actual = jpaQueryFactory .select(team.name, member.age.avg()) .from(member) .join(member.team, team) .groupBy(team.name) .having(team.name.eq("teamA")) .fetch(); // then Tuple teamA = actual.get(0); assertThat(teamA.get(team.name)).isEqualTo("teamA"); assertThat(teamA.get(member.age.avg())).isEqualTo(15); }
- select: team의 name과 member의 age의 평균을 조회
- from: member entity 대상
- join: member와 team을 inner join
- groupBy: team의 name을 그룹핑
- having: team의 name으로 그룹핑된 결과 중 "teamA"의 값을 가진 결과만 조회
- Query
# JPQL /* select team.name, avg(member1.age) from Member member1 inner join member1.team as team group by team.name having team.name = ?1 */ # SQL Query select team1_.name as col_0_0_, avg(member0_.age) as col_1_0_ from member member0_ inner join team team1_ on member0_.team_id=team1_.id group by team1_.name having team1_.name=?
5. 조인(Join)
5-1. 기본 조인
- join 메서드 이용
- 조인의 기본 문법은 첫 번째 파라미터에 조인 대상, 두 번째 파라미터에 별칭으로 사용할 Q-Type을 지정하면 된다
- JPQL join과 같다
- Code
// join (inner join) @Test void joinTest() { // when List<Member> members = jpaQueryFactory .selectFrom(member) .join(member.team, team) .where(team.name.eq("teamA")) .fetch(); // then assertThat(members) .hasSize(2) .extracting("userName", "age") // 대소문자 구분 조심 .containsExactly(Tuple.tuple("memberA", 10), Tuple.tuple("memberB", 20)); assertThat(members) .extracting("team") .extracting("name") .containsOnly("teamA"); }
- join 메서드는 기본적으로 inner join ( 명시적으로 innerJoin 메서드를 사용해도된다 )
- leftJoin 메서드는 left outer join, rightJoin 메서드는 right outer join 쿼리를 발생시킨다
- Query
# JPQL /* select member1 from Member member1 inner join member1.team as team where team.name = ?1 */ # SQL Query select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.user_name as user_nam3_1_ from member member0_ inner join team team1_ on member0_.team_id=team1_.id where team1_.name=?
5-2. Theta join ( Cross join )
- 카테시안 조인 ( N * M )
- FROM절의 명시된 모든 테이블의 데이터를 가져와 WHERE절 조건으로 필터링
- JPA의 연관관계가 설정되지 않았어도 join은 가능하다
- Code ( 살짝 억지스럽게.. member의 username과 Team의 name이 같은 경우로... )
// theta join @Test void thetaJoinTest() { // given em.persist(new Member("teamA")); em.persist(new Member("teamB")); em.persist(new Member("teamC")); em.flush(); em.clear(); // when List<Member> actual = jpaQueryFactory .select(member) .from(member, team) .where(member.userName.eq(team.name)) .fetch(); // then assertThat(actual) .hasSize(2) .extracting("userName") .containsExactly("teamA", "teamB"); }
- 명시적으로 join 같은 메서드를 호출하진 않았지만 from절에 두 개의 엔티티(테이블)이 들어간 것을 확인할 수 있다
- Query
# JPQL /* select member1 from Member member1, Team team where member1.userName = team.name */ # SQL Query select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.user_name as user_nam3_1_ from member member0_ cross join team team1_ where member0_.user_name=team1_.name
5-3. ON절
- ON절을 이용한 조인 ( JPA 2.1 부터 지원, Hibernate 5.1 이상 )
- 조인 대상 필터링
- 연관관계가 없는 엔티티 외부 조인 ( 해당 목적으로 자주 쓰임 )
- 단 inner join일 경우는 on절로 필터링 하는 것과 Where 조건절을 추가해서 필터링하는 것이 동일하다
- on절을 활용한 조인 대상 필터링을 사용할 때, 내부 조인(Inner join)이면 익숙한 where절로 해결하고, 정말 외부 조인(outer join)이 필요한 경우에만 on절 기능을 사용하자
- Code ( 조인 대상 필터링 케이스 )
// on절 @Test void joinOnFiltering() { // when List<com.querydsl.core.Tuple> actual = jpaQueryFactory .select(member, team) .from(member) .leftJoin(member.team, team) .on(team.name.eq("teamA")) // join해서 가져올지 말지 필터링 .fetch(); // then assertThat(actual).hasSize(4); List<Team> teams = actual.stream() .map(tuple -> tuple.get(team)) .filter(Objects::nonNull) .collect(Collectors.toList()); assertThat(teams) .hasSize(2) .extracting("name") .containsOnly("teamA"); }
- on절은 outer join한 결과를 가져올지 말지를 결정한다
- Query
# JPQL /* select member1, team from Member member1 left join member1.team as team with team.name = ?1 */ # SQL Query select member0_.member_id as member_i1_1_0_, team1_.id as id1_2_1_, member0_.age as age2_1_0_, member0_.team_id as team_id4_1_0_, member0_.user_name as user_nam3_1_0_, team1_.name as name2_2_1_ from member member0_ left outer join team team1_ on member0_.team_id=team1_.id and ( team1_.name=? )
- JPQL에서 left join ... with ... 구문을 확인할 수 있다
- SQL Query에서 left outer join ... on ... and ... , and 뒤에 붙은 조건이 on절로 지정해준 조건임을 확인할 수 있다
- Code ( 연관관계가 없는 엔티티 외부 조인 )
// 연관관계가 없다고 가정하고 left join @Test void joinWithoutRelationTest() { // given em.persist(new Member("teamA")); em.persist(new Member("teamB")); em.persist(new Member("teamC")); em.flush(); em.clear(); // when List<com.querydsl.core.Tuple> actual = jpaQueryFactory .select(member, team) .from(member) .leftJoin(team) // 연관관계 조인이랑 문법이 다르다. member.team이 아니라 team이다 .on(member.userName.eq(team.name)) .fetch(); // then List<Team> actualTeams = actual.stream() .map(innerActual -> innerActual.get(team)) .filter(Objects::nonNull) .collect(Collectors.toList()); assertThat(actualTeams) .hasSize(2) .extracting("name") .containsExactly("teamA", "teamB"); }
- leftJoin 메서드
- 기존에 Member와 Team의 연관관계로 join을 만들땐 leftjoin(member.team, team)으로 소속을 나타내줬다, 이럴 경우 SQL Query는 Member 엔티티의 @Id값과 Team 엔티티의 @Id값으로 조인을 한다
- 하지만 연관관계가 없는 경우 바로 Q-Type 클래스를 적어주며, 이는 on절로 명시된 조건으로만 join을 걸게 된다
- leftJoin 메서드
- Query
# JPQL /* select member1, team from Member member1 left join Team team with member1.userName = team.name */ # SQL Query select member0_.member_id as member_i1_1_0_, team1_.id as id1_2_1_, member0_.age as age2_1_0_, member0_.team_id as team_id4_1_0_, member0_.user_name as user_nam3_1_0_, team1_.name as name2_2_1_ from member member0_ left outer join team team1_ on ( member0_.user_name=team1_.name )
- 기존 연관관계를 이용하여(member.team) leftJoin을 사용했을 경우에는 아래처럼 join되는 entity의 id값 매칭과 on절 추가 조건이 매칭됐다
// on절 on member0_.team_id=team1_.id and ( team1_.name=? )
- 하지만 연관관계가 없기 때문에 on절의 조건이 단순히 on method에 정의해준 조건만 매칭된 것을 확인해볼 수 있다
- 기존 연관관계를 이용하여(member.team) leftJoin을 사용했을 경우에는 아래처럼 join되는 entity의 id값 매칭과 on절 추가 조건이 매칭됐다
5-4. FETCH JOIN
- SQL에서 제공하는 문법이 아닌 JPA에서 제공하는 fetch join 기능
- 연관관계로 맺어진 객체를 같이 조회하는 것 ( Lazy 상태의 연관관계 매핑인 엔티티들 대상 )
- Code
// fetch join @PersistenceUnit private EntityManagerFactory emf; @Test void fetchJoinUseTest() { // given em.flush(); em.clear(); // when Member findMember = jpaQueryFactory .selectFrom(member) .join(member.team, team) .fetchJoin() .where(member.userName.eq("memberA")) .fetchOne(); boolean loaded = emf.getPersistenceUnitUtil().isLoaded(findMember.getTeam()); assertThat(loaded) .as("페치 조인 미적용") .isTrue(); }
- @PersistenctUnit을 이용해 EntityManagerFactory를 주입
- EntityManagerFactory가 가지고있는 util중 isLoadded를 이용해 해당 객체가 로딩됐는지 아닌지를 판단할 수 있다
- join(), leftJoin(), rightJoin() 등 어떤것을 사용해도되며 뒤에 fetchJoin() 메서드만 추가해주면 된다
- Query
# JPQL /* select member1 from Member member1 inner join member1.team as team where member1.userName = ?1 */ # SQL Query select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.user_name as user_nam3_1_ from member member0_ inner join team team1_ on member0_.team_id=team1_.id where member0_.user_name=?
6. 서브 쿼리
- 쿼리안에 또 다른 쿼리를 넣는 방식
- com.querydsl.jpa.JPAExpressions 사용
- 사용하는 본 쿼리와 안의 서브 쿼리의 alias는 겹치면 안된다
- 예제 코드 1: 가장 나이가 많은 멤버 조회
// SubQuery @Test void subQueryTest() { // given // 서브쿼리에서 쓸 Q-Type과 본 쿼리 Q-Type이 겹치면 안된다 ( static Q-Type 쓰면 alias가 같음 ) // 서로의 alias가 달라야한다 QMember memberSub = new QMember("memberSub"); // when List<Member> actual = jpaQueryFactory .selectFrom(member) .where(member.age.eq( JPAExpressions .select(memberSub.age.max()) .from(memberSub) )) .fetch(); // then assertThat(actual) .hasSize(2) .extracting("userName", "age") .containsExactly(Tuple.tuple("memberB", 30), Tuple.tuple("memberC", 30)); }
- 예제 1. Query
# JPQL /* select member1 from Member member1 where member1.age = ( select max(memberSub.age) from Member memberSub ) */ # SQL Query select member0_.member_id as member_i1_1_, member0_.age as age2_1_, member0_.team_id as team_id4_1_, member0_.user_name as user_nam3_1_ from member member0_ where member0_.age=( select max(member1_.age) from member member1_ )
- 예제 2. Select절에 서브쿼리
// SubQuery @Test void selectSubQueryTest() { // given QMember memberSub = new QMember("memberSub"); // when List<com.querydsl.core.Tuple> tuples = jpaQueryFactory .select(member.userName, JPAExpressions .select(memberSub.age.avg()) .from(memberSub)) .from(member) .fetch(); // then tuples.forEach(System.out::println); }
- JPA 사용시 서브쿼리의 한계
- JPA에서는 FROM절에 서브쿼리가 안된다
- SELECT절이나 WHERE절만 가능
- JPA JPQL의 한계점으로 FROM절의 서브쿼리(인라인 뷰)는 지원하지 않는다, 당연히 QueryDSL도 지원하지 않음
- 원래 JPA 표준 스펙으로는 SELECT절 서브쿼리도 안된다. 하이버네이트 구현체로는 사용 가능
- FROM절 서브쿼리 해결방법
- 서브쿼리를 join으로 바꿀 수 있으면 변경한다 ( 가능한 상황도 있고, 불가능한 상황도 있음 )
- 어플리케이션에서 쿼리를 두 번으로 나눠서 실행한다 ( 상황에 따라 다름 )
- 저 두 가지로 해결이 안되면 nativeSQL을 써라
7. CASE문
- 표현 방식
- 기본 단순 표현식: 단순한 값으로 WHEN을 걸때
- CaseBuilder 이용: Q-Type을 이용해 좀 더 복잡하게 상황을 만들어야할 때
- 예제 코드
// simple case @Test void caseTest() { // when List<String> actual = jpaQueryFactory .select(member.age .when(10).then("열살") .when(20).then("스무살") .otherwise("기타")) .from(member) .fetch(); // then actual.forEach(System.out::println); }
- SQL Query
# JPQL /* select case when member1.age = ?1 then ?2 when member1.age = ?3 then ?4 else '기타' end from Member member1 */ # SQL Query select case when member0_.age=? then ? when member0_.age=? then ? else '기타' end as col_0_0_ from member member0_
- 예저 코드: CaseBuilder 사용
// CaseBuilder @Test void usingCaseBuilderTest() { // when List<String> actual = jpaQueryFactory .select(new CaseBuilder() .when(member.age.between(0, 20)).then("10살에서 20살") .when(member.age.between(21, 30)).then("21살에서 30살") .otherwise("기타")) .from(member) .fetch(); actual.forEach(System.out::println); }
- 추가
- 가급적.. 이런 Case문 같은건 DB에서 하지말자
- DB는 플랫한 데이터를 추출하는 용도로 사용하고 어플리케이션에서 분류하는 형태를 지향하자 ( 물론.. 성능상 case가 좋은 경우는 뭐... )
8. 상수, 문자 더하기
- 상수 사용 ( com.querydsl.core.types.dsl.Expressions 사용 )
// 상수 @Test void expressionsTest() { List<Tuple> actual = jpaQueryFactory .select(member.userName, Expressions.constant("A")) .from(member) .fetch(); actual.forEach(System.out::println); }
- 문자열 추가
// concat @Test void concatTest() { // when List<String> actual = jpaQueryFactory .select(member.userName.concat("_").concat(member.age.stringValue())) .from(member) .fetch(); // then actual.forEach(System.out::println); }
- member.userName과 member.age는 서로 다른 타입이다
- 따라서 member.age.stringValue()로 타입 캐스팅한 것을 확인할 수 있다 ( Enum 처리할 때 자주 사용된다 )
728x90
반응형