QueryDSL Part.2

dev_314·2023년 4월 4일
0

JPA - Trial and Error

목록 보기
15/16

프로젝션과 결과 반환

기본

List<String> fetch = qf.select(member.username).from(member).fetch();
List<Member> fetch = qf.selectFrom(member).fetch();

프로젝션 대상이 하나인 경우에는 타입을 명확히 지정할 수 있다.

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

for (Tuple tuple : fetch) {
	String username = tuple.get(member.username);
	Integer age = tuple.get(member.age);
}

프로젝션 대상이 둘 이상인 경우에는 Tuple을 사용해야 한다.

다른 계층에서는 특정 기술(QueryDSL)에 의존적이면 안 된다.
따라서 Tuple을 다른 계층에 넘기지 말고, DTO로 변환해서 전달해야 한다.

DTO

순수 JPA로 DTO를 다루려면 다음과 같을 것이다.

List<MemberDto> memberDtos = em.createQuery(
	"SELECT new 패키지명.MemberDto(m.username, m.age) FROM Member m",
    MemberDto.class
).getResultList();

순수 JPA로 DTO를 다룰 때의 단점

1. DTO의 패키지 명을 반드시 명시해야 한다.
2. 생성자 방식으로만 객체를 만들 수 있다.

QueryDSL에서는 세 가지 방법으로 DTO를 다룰 수 있다.

Projections.bean

List<MemberDto> fetch = qf
		.select(Projections.bean(MemberDto.class, 
				member.username, 
				member.age))
		.from(member)
		.fetch();
  1. 기본 생성자로 객체 생성 후, Setter를 통해 값을 할당한다.
  2. 따라서 기본 생성자Setter가 반드시 있어야 한다.

Projections.field

List<MemberDto> fetch = qf
		.select(Projections.fields(MemberDto.class,
				member.username,
				member.age))
		.from(member)
		.fetch();
  1. Setter를 사용하지 않고, 필드명을 인식하여 값을 할당한다.
  2. 따라서 Member Entity의 필드명과, DTO의 필드명이 일치해야 한다.

Member Entity의 필드명과, DTO의 필드명이 일치해야 한다.

만약 Member Entity는 username이라는 필드명을 사용하는데, MemberDtoname이라는 필드명을 사용하면 name필드에 null이 할당된다.

이렇게 필드명 불일치 문제를 다음과 같이 해결할 수 있다.

List<MemberDto> fetch = qf
		.select(Projections.fields(MemberDto.class,
				member.username.as("name"),
				member.age))
		.from(member)
		.fetch();

SELECT 절 Subquery에 별칭 부여하기

DTO + Projections.field를 사용하려면 DTO 필드명 일치 여부를 주의해야 한다.

따라서 Subquery도 필드명을 일치시켜야 하는데, 다음의 방법으로 할 수 있다.

QMember subMember = new QMember("subMember")

List<MemberDto> fetch = qf
		.select(Projections.fields(MemberDto.class,
        		// 위와 동일한 효과
				ExpressionUtils.as(member.username, "username"),
				ExpressionUtils.as( // subquery 결과를 필드로 사용
                	JPAExpressions // subquery 생성 구문
                    	.select(subMember.age.max())
                        .from(subMember),
                    "age" // alias
                ))
		.from(member)
		.fetch();

Projections.constructor

List<MemberDto> fetch = qf
		.select(Projections.constructor(MemberDto.class,
				member.username,
				member.age))
		.from(member)
		.fetch();
  1. 시그니처가 일치하는 생성자를 통해 객체를 생성한다.
  2. 따라서 fields와 달리 필드명은 중요하지 않다.

@QueryProjection

DTO를 직접 다루는 방식은 조금 복잡하다.
@QueryProjection Annotation으로 개선할 수 있다.

@Data
@NoArgsConstructor
public class MemberDto {
	
    private String username;
    private int age;
    
    @QueryProjection
    public MemberDto(String username, int age) {
    	this.username = username;
        this.age = age;
    }
}
  1. 생성자에 @QueryProjection을 붙인다
  2. compileQuerydsl로 컴파일 하면 DTO에도 Q-Type이 생성된다.

그런 뒤, 다음과 같이 사용하면 된다.

List<MemberDto> fetch = qf
		.select(new QMemberDto(member.username, member.age))
		.from(member)
		.fetch();

장단점

장점

Projections.consturctor는 컴파일 타임에 문제를 발견할 수 없다.

qf
.select(Projections.constructor(MemberDto.class,
		member.username,
        member.age,
        member.id
))

DTO에 존재하지 않는 member.id를 사용했음에도, 컴파일 타임에 문제를 검출하지 못하고 런타임에 문제가 발생할 수도 있다.

그런데 QueryProjection은 실제 생성자를 사용하는 것이므로, 컴파일 타임에 문제를 발견할 수 있다.

단점

  1. Q-Type을 생성, 관리해야 한다.
  2. QueryProjection은 QueryDSL의 기술이다.
    • 즉, DTO가 특정 기술에 종속적이게 되는 문제가 발생한다.
    • DTO는 여러 Layer에서 사용하는데, 이는 여러 Layer에서 QueryDSL에 종속적이게 되는 결과로 이어진다.

결론

의존성 문제를 우선한다면 Projection을 사용하자
편의성을 우선한다면 그냥 QueryProjection을 사용하자.
알아서 선택하라는 뜻

동적 쿼리

QueryDSL은 두 가지 방법으로 동적 쿼리를 작성한다.

BooleanBuilder

// 값이 없는 경우(null)에는 WHERE문에서 제외하고 싶은 상황
String username = "username1";
Integer age = 10;

BooleanBuilder booleanBuilder = new BooleanBuilder();
if (username != null) {
	booleanBuilder.and(member.username.eq(username));
}
if (age != null) {
	booleanBuilder.and(member.age.eq(age));
}

List<Member> fetch = qf
		.selectFrom(member)
        .where(booleanBuilder)
        .fetch();

Where 다중 파라미터

// 값이 없는 경우(null)에는 WHERE문에서 제외하고 싶은 상황
String username = "username1";
Integer age = 10;

List<Member> fetch = qf
		.selectFrom(member)
        .where(usernameEq(username), ageEq(age))
        .fetch();
        
private Predicate ageEq(Integer age) {
	if (age == null) {
		return null;
	}
	return member.age.eq(age);
}

private Predicate usernameEq(String username) {
	if (username == null) {
		return null;
	}
	return member.username.eq(username);
}
  1. WHERE의 파라미터가 NULL이면 무시된다는 특징을 활용한 방식
  2. 쿼리 재활용, 조립 가능
  3. 가독성

조건 조합을 위해, Predicate보다 BooleanExpression을 반환 타입으로 사용하는 것이 좋다.

private BooleanExpression usernameEq(String username) {...}

Where 다중 파라미터 사용 권장

Bulk Operation

Dirty Checking을 사용하지 않고 DB에 직접 쿼리를 날리자.
Persistence Context 불일치에 주의해야 한다.

  • Bulk Operation 수행 후에 Persistence Context flush, clear 필요
long count = qf
		.update(member)
		.set(member.username, "older than 30")
		.where(member.age.gt(30))
		.execute();

SQL Function

나중에 다루도록 하겠다,,,

profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글