Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 코드로 시작하는 자바 첫걸음
- 스프링 mvc1 - 스프링 mvc
- 스프링 mvc2 - 로그인 처리
- 자바의 정석 기초편 ch11
- 2024 정보처리기사 시나공 필기
- jpa 활용2 - api 개발 고급
- @Aspect
- 자바의 정석 기초편 ch5
- 자바의 정석 기초편 ch9
- 게시글 목록 api
- 스프링 db1 - 스프링과 문제 해결
- 자바의 정석 기초편 ch4
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch1
- 자바의 정석 기초편 ch12
- jpa - 객체지향 쿼리 언어
- 2024 정보처리기사 수제비 실기
- 자바의 정석 기초편 ch7
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch14
- 자바의 정석 기초편 ch2
- 자바의 정석 기초편 ch6
- 스프링 mvc2 - 타임리프
- 스프링 mvc2 - 검증
- 스프링 mvc1 - 서블릿
- 스프링 입문(무료)
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch3
- 자바의 정석 기초편 ch13
Archives
- Today
- Total
나구리의 개발공부기록
중급 문법, 프로젝션과 결과 반환(기본/DTO 조회/@QueryProjection), 동적 쿼리(BooleanBuilder사용/Where다중 파라미터 사용), 수정 및 삭제 벌크 연산, SQL function 호출하기 본문
인프런 - 스프링부트와 JPA실무 로드맵/실전! Querydsl
중급 문법, 프로젝션과 결과 반환(기본/DTO 조회/@QueryProjection), 동적 쿼리(BooleanBuilder사용/Where다중 파라미터 사용), 수정 및 삭제 벌크 연산, SQL function 호출하기
소소한나구리 2024. 10. 30. 11:42출처 : 인프런 - 실전! Querydsl (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 프로젝션과 결과 반환 - 기본
** 프로젝션: select에 대상을 지정하는 것
1) 프로젝션 대상이 하나
- 프로젝션 대상이 하나면 타입을 명확하게 지정할 수 있음
- String뿐만 아니라 반환타입에 객체타입을 명확하게 지정할 수 있음
// 프로젝션 대상이 하나
@Test
void simpleProjection() {
List<String> result = queryFactory
.select(member.username)
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
2) 튜플 조회
- 프로젝션 대상이 둘 이상일 때 사용하는 방법 중 하나(또 다른 방법은 DTO로 조회 - 실무에서 주로 사용)
- tuple의 값을 꺼낼때는 get()으로 select절에 입력한 프로젝션을 그대로 입력해주면 됨
- 튜플은 com.querydsl.core.Tuple에 있기때문에 repository계층에서 사용하는 것은 괜찮을 수 있으나 service나 화면계층으로 넘거아는 것은 좋지 않은 설계임
- repository 계층의 코드가 외부의 계층에 종속적이면 안좋은 설계인 것과 동일한 개념으로 이해하면 됨
// 프로젝션 대상이 둘 - tuple
@Test
void tupleProjection() {
List<Tuple> result = queryFactory
.select(member.username, member.age)
.from(member)
.fetch();
for (Tuple tuple : result) {
System.out.println("username) = " + tuple.get(member.username));
System.out.println("age = " + tuple.get(member.age));
}
}
2. 프로젝션과 결과 반환 - DTO
1) DTO 생성
- Member의 전체 필드가아닌 username과 age만 담는 클래스 생성
@Data
@NoArgsConstructor
public class MemberDto {
private String username;
private int age;
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
2) DTO 직접 조회
(1) 순수 JPA에서 DTO조회 - new 연산자를 활용해야함
- 과거 강의에서 계속 해왔던 JPQL로 DTO를 직접 조회하려면 마치 생성자를 입력하듯이 new 연산자를 사용해서 DTO가 속해있는 패키지명을 전부 입력해주고 인자값으로 필드명을 입력해주어야함
- 패키지명을 전부 입력해줘야하는 것이 상당히 번거로우며 생성자 방식만 지원하기때문에 생성자를 꼭 생성해줘야함
// 프로젝션 대상이 둘 - DTO(JPQL쿼리, new 연산자를 적용해야함)
@Test
void findDtoByJPQL() {
List<MemberDto> resultList = em.createQuery("select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
.getResultList();
for (MemberDto memberDto : resultList) {
System.out.println("memberDto = " + memberDto);
}
}
(2) Querydsl로 DTO 조회
- 순수 JPA에서 DTO로 조회했을 때의 문제를 전부 해결하고 Projections의 .bean, .fields, .constructor를 활용하여 프로퍼티 접근, 필드 직접 접근, 생성자 접근 모두 가능함
- 세터, 생성자를 모두 만들어서 브레이크포인트로 디버그를 찍어보면 bean()은 setter, fields()는 필드로 직접, constructor()는 생성자로 접근하여 값을 입력하는 것을 알 수 있음
- 인자로 조회할 DTO클래스.class로 타입을 입력해 주어야 함
- 실무에서는 아래에서 배울 @QueryProjection방식이 아니면 생성자 방식을 가장 많이 사용함
// 프로젝션 대상이 둘 - DTO(Querydsl)
@Test
void findDtoByQuerydsl() {
// setter를 이용
List<MemberDto> resultSetter = queryFactory
.select(Projections.bean(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : resultSetter) {
System.out.println("memberDto = " + memberDto);
}
// field에 직접 접근 - Q클래스와 DTO의 필드명이 동일
List<MemberDto> resultFields = queryFactory
.select(Projections.fields(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : resultFields) {
System.out.println("memberDto = " + memberDto);
}
// 생성자 방식
List<MemberDto> resultConstructor = queryFactory
.select(Projections.constructor(MemberDto.class,
member.username,
member.age))
.from(member)
.fetch();
for (MemberDto memberDto : resultConstructor) {
System.out.println("memberDto = " + memberDto);
}
}
(3) Querydsl - Q클래스와 DTO의 필드명이 다를 때(별칭이 다를 때)
- UserDto라는 클래스에는 username이아니라 필드명이 name이라 필드명이 다르면 SQL문법처럼 필드명에 .as()로 타입으로 지정한 DTO의 필드를 별칭으로 지정하면 정상적으로 조회가 됨
- 잘 사용하진 않지만 필드는 물론 서브쿼리에 별칭을 적용할 때에는 ExpressionUtils.as()를 활용하면 됨
- 그러나 필드명에 별칭을 사용해야한다면 굳이 ExpressionUtils.as()를 사용할 필요는 없이 필드명에 바로 .as()를 사용하는것이 훨씬 깔끔함
// field에 직접 접근 - Q클래스와 DTO의 필드명이 다를 때
List<UserDto> resultFields2 = queryFactory
.select(Projections.fields(UserDto.class,
member.username.as("name"),
member.age))
.from(member)
.fetch();
for (UserDto userDto : resultFields2) {
System.out.println("memberDto = " + userDto);
}
// field에 직접 접근 - 필드나, 서브쿼리에 별칭을 적용
QMember memberSub = new QMember("memberSub");
List<UserDto> resultFields3 = queryFactory
.select(Projections.fields(UserDto.class,
// 필드에 별칭
member.username.as("name"),
// 서브쿼리에 별칭
ExpressionUtils.as(JPAExpressions
.select(memberSub.age.max())
.from(memberSub), "age")
))
.from(member)
.fetch();
for (UserDto userDto : resultFields3) {
System.out.println("memberDto = " + userDto);
}
3. 프로젝션과 결과 반환 - @QueryProjection
1) 예제 코드
(1) DTO의 생성자에 @QueryProjection 추가
- DTO의 생성자에 @QueryProjection 애노테이션을 작성 후 gradle - clean, build를 하면 DTO클래스도 Q파일이 생성됨
public class MemberDto {
// ... 기존코드 생략
@QueryProjection // 생성자에 애노테이션 작성
public MemberDto(String username, int age) {
this.username = username;
this.age = age;
}
}
(2) 적용
- DTO에 Querydsl 애노테이션을 작성해야하고 DTO까지 Q파일을 생성해야한다는 단점이 존재하지만 Q클래스를 new 연산자를 사용하여 쿼리를 입력하면 타입체크가 되기 때문에 안전하게 작성할 수 있음
- 생성자 방식으로 DTO를 직접 조회하는 방식은 런타임 시점에 오류를 확인할 수 있지만 @QueryProjection을 적용한 방식은 생성자 방식과 비슷하지만 컴파일 시점에 오류를 잡을 수 있기 때문에 개발하기가 수월함
- 아키텍처 설계를 할 때 DTO를 순수하게 유지하고 설계하는 프로젝트의 경우 DTO가 Querydsl을 의존을 하기때문에 적용하기가 어려울 수 있음
// @QueryProjection 적용
@Test
void findDtoByQueryProjection() {
List<MemberDto> result = queryFactory
.select(new QMemberDto(member.username, member.age))
.from(member)
.fetch();
for (MemberDto memberDto : result) {
System.out.println("memberDto = " + memberDto);
}
}
** distinct
- Querydsl의 distinct는 JPQL과 동일기능으로 동일하게 사용할 수 있음
- select(member.username).distinct() ... 과 같이 사용하면됨
4. 동적 쿼리 - BooleanBuilder 사용
1) 예제 코드
- Querydsl의 동적쿼리를 해결하는 방법 중 BooleanBuilder()를 생성하여 해결하는 방법
- 동적쿼리는 파라미터에 따라 쿼리의 조건이 달라져야하므로 메서드로 별도로 빼서 작성하고 BooleanBuilder()를 생성하여 참조변수를 선언
- 자유롭게 작성이 가능하지만 기본적으로 각 파라미터의 값이 null이 아닐 때 조건이 발생되도록 작성하는 경우가 많으므로 if문으로 null이 아니면 builder에 메서드 체인으로 적용하려는 조건을 작성
- 각 파라미터마다 조건의 작성이 모두 끝나면 builder를 where문의 인자로 입력해주면 동적쿼리가 완성됨
- 아래의 예제에서는 파라미터의 값이 모두 일치하는 값을 조회하는 예제를 작성하였고 실행해보면 쿼리결과가 정상적으로 where문의 두 조건이 입력되며 둘중 파라미터의 값이 하나라도 null이면 해당 조건은 빠짐
- BooleanBuilder를 생성할 때 인수값으로 조건을 입력하여 필수로 적용되게 할 수도 있음(예외 처리 필요)
// 동적쿼리 - BooleanBuilder
@Test
void dynamicQuery_BooleanBuilder() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember1(usernameParam, ageParam);
assertThat(result.get(0))
.extracting("username", "age")
.containsExactly(usernameParam, ageParam);
}
// 동적쿼리 메서드
private List<Member> searchMember1(String usernameCond, Integer ageCond) {
BooleanBuilder builder = new BooleanBuilder(); // 생성시 인수에 값을 입력하여 초기값을 설정할 수 있음
if (usernameCond != null) { // usernameCond가 null이 아니면 .and(조건)이 입력
builder.and(member.username.eq(usernameCond));
}
if (ageCond != null) {
builder.and(member.age.eq(ageCond));
}
return queryFactory
.selectFrom(member)
.where(builder) // where에 조건들이 입력된 builder를 입력
.fetch();
}
5. 동적 쿼리 - Where 다중 파라미터 사용
1) 예제 코드
- where문에 각 파라미터의 조건을 처리하는 메소드를 작성하여 입력하는 방식이며 완성된 메인 query의 구조가 일반적으로 자주 접하는 SQL과 흡사한 형태로 조건이 들어가있기 때문에 가독성이 좋음
- .where조건에는 null의 값은 무시되기 때문에 편리하게 파라미터의 조건을 처리하는 메서드를 작성하기가 수월함
- 또한 조건 자체가 메서드이기때문에 작성된 조건들을 원하는 대로 합치거나 분리하거나 부분적으로 적용하는등의 커스텀 메서드를 만들어서 where문에 간단히 적용할 수도있고 다른 query에서도 조건들을 재사용할 수 있는 장점이 있음
- 실무에서 동적쿼리를 적용할때 해당 방식을 적용하는 것을 권장함
// 동적쿼리 - Where 다중 파라미터 사용(실무에서 사용하기 좋음)
@Test
void dynamicQuery_WhereParam() {
String usernameParam = "member1";
Integer ageParam = 10;
List<Member> result = searchMember2(usernameParam, ageParam);
assertThat(result.get(0))
.extracting("username", "age")
.containsExactly(usernameParam, ageParam);
}
// 동적쿼리 메서드 - BooleanBuilder보다 쿼리의 가독성이 SQL과 비슷하여 가독성이 좋음
private List<Member> searchMember2(String usernameCond, Integer ageCond) {
return queryFactory
.selectFrom(member)
.where(usernameEq(usernameCond), ageEq(ageCond)) // where문에 null이 반환되면 그냥 무시됨
.fetch();
}
// 직접 조건을 입력하는 메서드를 작성
// 메서드의 반환타입은 Predicate보다 BooleanExpression을 사용하는 것이 더 활용하기 좋음
private BooleanExpression usernameEq(String usernameCond) {
// null 이면 null을 그냥 반환하고 null이 아니면 조건이 반환
if (usernameCond != null) {
return member.username.eq(usernameCond);
} else {
return null;
}
}
// 간단한 조건은 3항연산자로 만드는 것을 추천
private BooleanExpression ageEq(Integer ageCond) {
return ageCond != null ? member.age.eq(ageCond) : null;
}
// 여러가지의 조건을 조립하여 합치거나 분리하는 등의 원하는 조건끼리 분리하여 적용할 수 있음(null 체크는 주의해서 처리해야함)
private BooleanExpression allEq(String usernameCond, Integer ageCond) {
return usernameEq(usernameCond).and(ageEq(ageCond));
}
// return queryFactory
// .selectFrom(member)
// .where(allEq(usernameCond, ageCond)) 조립한 메서드를 where 문에 적용
// .fetch();
6. 수정, 삭제 벌크 연산
1) 예제 코드
** 주의
- 벌크연산은 DB에 직접 쿼리를 날리기 때문에 벌크연산을 단독으로 사용하여 트랜잭션을 완료하던지 다른 JPA코드들과 함께 사용하려면 벌크연산 후 강제로 초기화를 해주어야만 데이터 정합성에 문제가 발생하지 않음
- 계속 반복해서 JPA에서 벌크연산을 사용할 때 주의점을 설명하는 이유는 매우 중요하기 때문임
(1) 쿼리 한번으로 대량 데이터 수정
- query의 마지막에 execute()를 사용하면 벌크연산으로 수행됨
// 벌크연산 - 수정, 항상 벌크연산은 DB에 바로 쿼리를 날리기 때문에 주의해야함
@Test
void bulkUpdate() {
// member1, member2 -> 잼민이들로 변경됨
// member3, member4 -> 유지
long count = queryFactory
.update(member)
.set(member.username, "잼민이들")
.where(member.age.loe(20))
.execute(); // 벌크연산 적용
// 벌크연산은 꼭 영속성컨텍스트를 초기화해주거나 트랜잭션 커밋을 해주어야함
em.flush();
em.clear();
List<Member> result = queryFactory.selectFrom(member).fetch();
for (Member member1 : result) {
System.out.println("member1 = " + member1);
}
}
(2) 기존 숫자에 1 더하기, 곱하기
- add() - 덧셈, 음수를 입력하면 뺄셈이 됨
- multiply() - 곱셈
- divide() - 나눗셈
- subtract() - 뺄셈
- 쿼리를 확인해보면 정상적으로 사칙연산이 적용되는 쿼리가 실행됨
// 별크연산 - 기존숫자에 사칙연산 수행
@Test
void bulkOperation() {
// 덧셈
queryFactory
.update(member)
.set(member.age, member.age.add(1))
.where(member.age.loe(20))
.execute();
// 곱셈
queryFactory
.update(member)
.set(member.age, member.age.multiply(2))
.where(member.age.gt(20))
.execute();
// 나눗셈
queryFactory
.update(member)
.set(member.age, member.age.divide(2))
.where(member.age.mod(2).eq(0)) // 짝수일때 조건, 2로 나눈 나머지가 0이면
.execute();
// 뺄셈, add(음수)를 적용해도 됨
queryFactory
.update(member)
.set(member.age, member.age.subtract(1))
.where(member.age.loe(15))
.execute();
}
(3) 쿼리 한번으로 대량 데이터 삭제
- 쿼리를 확인해보면 delete 쿼리가 정상적으로 수행됨
// 벌크연산 - 쿼리한번에 데이터 삭제
@Test
void bulkDelete() {
queryFactory
.delete(member)
.where(member.age.gt(15))
.execute();
}
7. SQL function 호출하기
1) 예제 코드
- Expressions.stringTemplate()를 호출하여 문자로 함수의 기능을 입력한뒤, 두번째 파라미터로 적용 대상을 입력할 수 있음
- 데이터베이스의 함수를 호출하여 사용할 수 있으며 기본적으로 JPA와 동일하게 Dialect(방언설정)에 등록된 내용만 호출할 수 있음
- 등록되지 않은 함수를 사용하려면 직접 각DB의 Dialect 클래스를 상속받아서 함수를 등록할 수 있음(https://nagul2.tistory.com/338)
- ANSI 표준 SQL의 함수들은 Querydsl에서도 메서드로 대부분 정의해두었기 때문에 where문에서 바로 필요한 함수를 호출하여 바로 사용할 수 있음
// SQL function 호출
@Test
void sqlFunction() {
List<String> result = queryFactory
.select(Expressions.stringTemplate("function('replace', {0}, {1}, {2})",
member.username, "member", "M"))
.from(member)
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}
}
@Test
void sqlFunction2() {
List<String> result = queryFactory
.select(member.username)
.from(member)
// .where(member.username.eq(Expressions.stringTemplate("function('lower', {0})",
// member.username)))
// ANSI표준에서 제공하는 기능들은 이미 메서드로 제공되어있음
.where(member.username.eq(member.username.lower()))
.fetch();
for (String s : result) {
System.out.println("s = " + s);
}