반응형
본 포스트는 인프런 - 실전! QueryDSL의 강의를 듣고 공부한 내용입니다
https://www.inflearn.com/course/Querydsl-%EC%8B%A4%EC%A0%84
1. 프로젝션과 결과 반환
- 프로젝션
- SELECT절에서 어떤 것을 가져올지 대상을 지정하는 것
- 대상이 한개의 타입
... List<String> actual = queryFactory .select(member.username) .from(member) .fetch(); ...
- 반환되는 타입이 한개 ( String type )인 경우는 컬렉션의 제네릭 타입을 명확하게 지정할 수 있음
- 한개의 Q-Type 클래스도 같은 방식으로 사용할 수 있음
- 대상이 두개 이상의 타입을 갖는 경우 튜플 혹은 DTO로 조회해야한다
- 튜플 조회
- 프로젝션 대상 타입이 둘 이상일 경우 사용
- 한번에 여러 타입을 담아, 꺼내서 쓸 수 있다
- com.querydsl.core.Tuple
- 튜플은 Repository 계층에서까지만 쓰는 것이 좋은 것 같다, Service, Controller 계층까지 넘어가는 것은 좋은 설계가 아닌 것 같음
- QueryDSL의 core 패키지란 뜻은 QueryDSL의 세부 기술이라는 뜻으로 infrastructure 계층, Repository를 넘어서서 다른 계층까지 침투하는 것은 좋지 않다
- 예제 코드
// Tuple @Test void tupleTest() { // when List<Tuple> actual = jpaQueryFactory .select(member.userName, member.age) .from(member) .fetch(); // then actual.forEach(el -> { String userName = el.get(member.userName); Integer age = el.get(member.age); System.out.println("userName => " + userName + ", age => " + age); }); }
- com.querydsl.core.types.Expression, 즉 select절에 넣어줬던 Q-Type의 Expression를 키로 결과를 저장하고 있다
- 꺼내올때도 저장했던 키 그대로 넣어주면, get 메서드를 통해 결과값을 가져올 수 있다
- DTO 조회
- 기존의 JPQL에서 DTO를 사용하려면 new Operation을 통해 실제 모든 패키지 경로를 SELECT절에 넣어줘야했다
- 하지만 이와다르게 QueryDSL은 결과를 DTO로 반환할 때 프로퍼티 접근, 필드 직접 접근, 생성자 사용이라는 3가지 방식을 전부 지원한다
- 프로퍼티 접근
- com.querydsl.core.types.Projections 이용
- Projections.bean()을 사용하여 지정해준 타입의 setter()로 인젝션하겠다고 명시
- setter()를 사용하겠다는 것(프로퍼티로 접근)은 기본 생성자로 newInstance를 한 후에 호출을 하겠다는 것으로 프로퍼티 접근을 사용하려면 기본 생성자가 있어야한다
- 예제 코드
// 프로퍼티 접근 @Test void findDtoBySetterTest() { // when List<MemberDto> actual = jpaQueryFactory .select(Projections.bean(MemberDto.class, member.userName, member.age)) .from(member) .fetch(); actual.forEach(System.out::println); }
- 필드 직접 접근
- Projections.fields()을 사용하여 지정해준 타입의 필드에 바로 값을 넣겠다는 뜻
- bean() 메서드와 동일하게 기본 생성자는 필수이지만 Setter() 메서드는 필요없다
- 매칭되는 프로퍼티가 없으면 에러가 나는게 아니라 무시된다!! ( 필드에 NULL값 ) 조심해야한다
- 예제 코드
// 필드 주입 @Test void findDtoByFieldsTest() { // when List<MemberDto> actual = jpaQueryFactory .select(Projections.fields(MemberDto.class, member.userName, member.age)) .from(member) .fetch(); actual.forEach(System.out::println); }
- 생성자 사용
- Projections.constructor()를 이용하여 정의된 생성자를 사용하는 방식
- 반드시 파라미터의 순서와 타입이 제공되는 생성자의 파라미터 순서, 타입과 일치해야한다
- Setter()나 기본 생성자는 필요없다
- 예제 코드
// 생성자 사용 @Test void findDtoByConstructorTest() { // when List<MemberDto> actual = jpaQueryFactory .select(Projections.constructor(MemberDto.class, member.userName, member.age)) .from(member) .fetch(); actual.forEach(System.out::println); }
- member.userName(String), member.age(Integer)의 순서와 타입으로 받는 Contructor가 MemberDto에 정의되어 있어야함
- 추가 사항: Q-Type 클래스의 속성명과 주입하려는 DTO의 속성명이 다른 경우는?
- 파라미터로 들어가는 Q-Type Class 속성에 as() 메서드를 이용해서 별칭을 적어주면 된다
- 예제 코드
// 별칭 지정 @Test void findUserDtoTest() { // when List<UserDto> actual = jpaQueryFactory .select(Projections.fields(UserDto.class, member.userName.as("name"), member.age.as("fakeAge"))) .from(member) .fetch(); actual.forEach(System.out::println); }
- 추가사항: 서브 쿼리의 결과가 DTO의 속성으로 주입하기
- 서브쿼리, com.querydsl.core.types.ExpressionsUtils 이용
- 예제 코드
// 서브쿼리 이용 @Test void usingSubQueryTest() { QMember memberSub = new QMember("memberSub"); List<UserDto> actual = jpaQueryFactory .select(Projections.fields(UserDto.class, member.userName.as("name"), ExpressionUtils.as(jpaQueryFactory .select(memberSub.age.max()) .from(memberSub), "fakeAge"))) .from(member) .fetch(); actual.forEach(System.out::println); }
- @QueryProjection 사용
- com.querydsl.core.annotations.QueryProjection
- 생성자에 @QueryProjection을 선언하여 DTO 또한 Q-Type 클래스가 생성되게 한다 ( Gradle 컴파일 필요 )
- 예제 코드
// Q-Type QueryProjection @Test void findDtoByQueryProjectionTest() { List<MemberDto> actual = jpaQueryFactory .select(new QMemberDto(member.userName, member.age)) .from(member) .fetch(); actual.forEach(System.out::println); }
- MemberDto 생성자에 @QueryProjection을 선언하여 QMemberDto를 생성했습니다
- 생성자 사용 방식(Projections.constructor(...)) vs @QueryProjection 차이
- 생성자 사용 방식은 런타임 시점에서 맞는 생성자를 찾고 없으면 오류를 낸다
- @QueryProjection으로 생성된 Q-Type Class는 컴파일 시점에서 오류를 낼 수 있다 ( 생성된 생성자만 사용하므로 )
- 실무상 단점
- Q-Type을 생성해야한다, @QueryProjection을 선언해야함
- DTO가 구체적인 외부 기술인 QueryDSL의 의존성을 가지고 있게된다 ( 선택을 해야할듯... )
2. 동적 쿼리
- 쿼리를 캐싱하지 않고 동적으로 SQL을 생성하는 것을 동적 쿼리라고 합니다 ( 조건에 따라 WHERE 변경등 )
- BooleanBuilder 사용 방법
- com.querydsl.core.BooleanBuilder
- 예제 코드
// BooleanBuilder @Test void booleanBuilderTest() { String usernameParam = "memberA"; Integer ageParam = 10; List<Member> result = selectMember1(usernameParam, ageParam); assertThat(result) .hasSize(1); } private List<Member> selectMember1(String usernameParam, Integer ageParam) { BooleanBuilder booleanBuilder = new BooleanBuilder(); if (Objects.nonNull(usernameParam)) { booleanBuilder.and(member.userName.eq(usernameParam)); } if (Objects.nonNull(ageParam)) { booleanBuilder.and(member.age.eq(ageParam)); } return jpaQueryFactory .selectFrom(member) .where(booleanBuilder) .fetch(); }
- BooleanBuilder에서 지원하는 메서드를 통해 조건별로 쿼리를 생성할 수 있다
- BooleanBuilder끼리도 연결할 수 있다
- Where 다중 파라미터 사용
- jpaQueryFactory의 where() 메서드는 기본적으로 들어오는 파라미터(Predicate)들을 and 조건으로 연결한다
- 단 null일때는 그냥 무시한다 ( 이것을 이용해 동적 쿼리를 만들 수 있다 )
- 예제 코드
// 다중 where 조건 @Test void whereTest() { String usernameParam = "memberA"; Integer ageParam = null; List<Member> result = selectMember2(usernameParam, ageParam); assertThat(result).hasSize(1); } private List<Member> selectMember2(String usernameParam, Integer ageParam) { return jpaQueryFactory .selectFrom(member) .where(usernameEq(usernameParam), ageEq(ageParam)) // 기본적으로 and조건, null은 무시 .fetch(); } private Predicate usernameEq(String usernameParam) { if (Objects.isNull(usernameParam)) { return null; } return member.userName.eq(usernameParam); } private Predicate ageEq(Integer ageParam) { if (Objects.isNull(ageParam)) { return null; } return member.age.eq(ageParam); }
- 3항 연산자를 통해 한줄로 처리하는 방식도 권장한다
- 각 조건별 메서드(usernameEq, ageEq)의 반환형을 Predicate로 했는데 조합을 위해 BooleanExpression으로 반환하는 것이 좋다
- 장점
- 만들어둔 조건절 메서드는 다른 쿼리에서도 재활용할 수 있다는 장점이 있다
- 쿼리 자체의 가독성이 좋아진다 ( 메서드로 추출하여 네이밍이 가능함 )
- 다양하게 조합이 가능하다 ( BooleanExpression 사용 )
3. 수정, 삭제 배치 쿼리
- 수정과 삭제에 대해 건건히 쿼리를 날리는 것이 아닌 벌크 연산을 통해 한번에 쿼리를 날릴 수 있습니다
- 수정
- JPA는 기본적으로 Update에 관해 변경감지가 동작하여 알아서 수정한다 ( 스냅샷과 비교 후 )
- fetch() 메서드를 통해 결과를 가져오는 것이 아닌 execute() 메서드를 통해 실행시킨다
- QueryDSL을 이용하여 벌크 연산 처리
// Bulk Update @Test void bulkUpdateTest() { // when long lowCount = jpaQueryFactory .update(member) .set(member.userName, "비회원") .where(member.age.lt(28)) .execute(); // then assertThat(lowCount).isEqualTo(2); }
- 벌크 연산은 영속성 컨텍스트의 내용을 무시하고, DB에 바로 쿼리가 날라간다 ( 주의 사항 )
// Bulk Update 주의사항 @Test @Commit void bulkUpdateTest() { // given // 3 memberA 10 // 4 memberB 20 // 5 memberC 30 // 6 memberD 40 // when long lowCount = jpaQueryFactory .update(member) .set(member.userName, "비회원") .where(member.age.lt(28)) .execute(); // then assertThat(lowCount).isEqualTo(2); // 3 비회원 10 // 4 비회원 20 // 5 memberC 30 // 6 memberD 40 // when List<Member> members = jpaQueryFactory .selectFrom(member) .fetch(); members.forEach(System.out::println); }
- id, username, age 순의 데이터 4개가 insert되어있다는 가정하에 위와 같은 코드를 실행시켜봤다
- 28살 미만의 모든 Member.username을 "비회원"으로 변경하는 벌크 Update를 실행 후 다시 전체 Member 테이블을 조회해봤다 ( 기존의 Member 데이터 4개는 같은 영속성 컨텍스트 내에서 insert 됐다고 가정한다 )
- 그렇다면 최종 forEach로 출력되는 결과는 무엇일까?
-
- 신기하게도 DB의 값은 정상적으로 변했지만 SELECT 쿼리로 실행된 결과는 변경되지 않았다
- 이유는 영속성 컨텍스트의 특징에 있다
- 영속성 컨텍스트는 같은 ID 값의 동일한 엔티티가 이미 1차 캐시에 존재할 경우 SELECT되어 결과가 조회되더라도 늦게 들어온 데이터를 버린다 ( 덮어쓰지 않는다, 영속성 컨텍스트의 기존 데이터가 항상 우선권을 갖는다 )
- 위와 같은 현상을 REPEATABLE READ(같은 쿼리를 여러번 실행해도 같은 결과를 내는 것)라고 한다
- 해결책
- 벌크 연산을 수행하면 항상 영속성 컨텍스트를 날려줘라
- entityManager.flush();
- entityManager.clear();
- Spring Data JPA, @Modifying
- 벌크 연산은 영속성 컨텍스트의 내용을 무시하고, DB에 바로 쿼리가 날라간다 ( 주의 사항 )
- 수정 ( 특정 값을 연산 )
- 예제 코드
// Update long execute = jpaQueryFactory .update(member) .set(member.age, member.age.add(1)) .execute();
- Q-Type 프로퍼티의 메서드(add, multiply ...)를 이용한다
- SQL Query
# JPQL /* update Member member1 set member1.age = member1.age + ?1 */ # SQL Query update member set age=age+?
- 예제 코드
- 삭제
- 예제 코드
// delete @Test void bulkDeleteTest() { long execute = jpaQueryFactory .delete(member) .where(member.age.gt(18)) .execute(); }
- SQL Query
# JPQL /* delete from Member member1 where member1.age > ?1 */ # SQL Query delete from member where age>?
- 예제 코드
4. SQL function 호출하기
- 기본적으로 Dialect에 등록되어있는 SQL 함수만 사용 가능하다
- 사용 가능한 함수(registerFunction())는 org.hibernate.dialect 패키지안 각 Dialect에 맞는 클래스에 정의되어있다 ( H2Dialect,
- com.querydsl.core.types.dsl.Expressions 사용
- 일반적인 Ansi 표준 문법들은 어느정도 Q-Type에 정의되어있긴하다 ( lowercase 등등 )
- 예제 코드 ( H2 Database만 사용 가능 )
// only H2 @Test void replaceTest() { List<String> actual = jpaQueryFactory .select(Expressions.stringTemplate( "function('replace', {0}, {1}, {2})", member.userName, "member", "M")) .from(member) .fetch(); actual.forEach(System.out::println); }
728x90
반응형
'Study > 실전! QueryDSL' 카테고리의 다른 글
Day 6. Spring Data JPA에서 지원하는 QueryDSL (0) | 2021.10.07 |
---|---|
Day 5. Spring Data JPA와 QueryDSL (0) | 2021.10.06 |
Day 4. 순수 JPA와 QueryDSL (0) | 2021.10.05 |
Day 2. QueryDSL 기본 문법 (0) | 2021.09.24 |
Day 1. QueryDSL 소개 및 프로젝트 설정 (0) | 2021.09.19 |