사용 이유

사용해 보고 느낀점은

  1. 쿼리를 java 코드로
    런타임 에러를 잡을 수 있다
    중복되는 쿼리를 재사용할 수 있다

  2. 동적 쿼리
    동적 쿼리를 다뤄본 적이 없어서 몰랐는데 이번 기회로 알게 되었다.
    쉽게 말해 분기에 따라 쿼리가 달라지는 경우.
    1번과 비슷하다

설정

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.3.2'
	id 'io.spring.dependency-management' version '1.1.6'
	id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
}

group = 'study'
version = '0.0.1-SNAPSHOT'

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(17)
	}
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

	implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'

	implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
}

tasks.named('test') {
	useJUnitPlatform()
}

clean {
	delete file('src/main/generated')
}

플러그인, 의존성, clean 옵션 추가

사용법

기본적인건 생략

Projection

    @Test
    public void simpleProjection() {
        List<String> result = queryFactory
                .select(member.username)
                .from(member)
                .fetch();

        for (String s : result) {
            System.out.println("s = " + s);
        }
    }

    @Test
    public void tupleProjection() {
        List<Tuple> result = queryFactory
                .select(member.username, member.age)
                .from(member)
                .fetch();

        for (Tuple tuple : result) {
            String username = tuple.get(member.username);
            Integer age = tuple.get(member.age);
            System.out.println("username = " + username);
            System.out.println("age = " + age);
        }
    }

현재 프로젝트에서 사용중인 jpql


    @Query("SELECT s.restaurantId " +
            "FROM UserRestaurantScrap s " +
            "WHERE s.userId = :userId " +
            "ORDER BY s.createdAt DESC")
    List<String> findAllRestaurantIdByUserId(String userId);

여기까진 별 감흥이 없다

DTO 변환

JPQL

    @Test
    public void findDtoByJPQL() {
        List<MemberDto> result = em.createQuery(
                "select new study.querydsl.dto.MemberDto(m.username, m.age) from Member m", MemberDto.class)
                .getResultList();

        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }

QueryDSL

@Test
    public void findDtoBySetter() {
        List<MemberDto> result = queryFactory
                .select(Projections.bean(MemberDto.class,
                                         member.username,
                                         member.age
                ))
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }


    @Test
    public void findDtoByField() {
        List<MemberDto> result = queryFactory
                .select(Projections.fields(MemberDto.class,
                                         member.username,
                                         member.age
                ))
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }


    @Test
    public void findDtoByConstructor() {
        List<MemberDto> result = queryFactory
                .select(Projections.constructor(MemberDto.class,
                                               member.username,
                                               member.age
                ))
                .from(member)
                .fetch();

        for (MemberDto memberDto : result) {
            System.out.println("memberDto = " + memberDto);
        }
    }


    @Test
    public 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);
        }
    }

여기서부터는 조금 편리할수도 있겠다 생각했다.
하지만 지금까지는 Repository에서는
Entity 혹은 List로 데이터를 꺼내는 경우밖에 없어서 크게 와닿지는 않았다.

하지만 프로젝트에서 DTO로 꺼내야 하는 경우가 있다면 확실히 편리할 것 같다.
그래도 아직까지는 굳이?

동적쿼리

이름, 나이가 주어지면 이름과 나이가 같은 회원을 조회
주어지지 않으면 조건에서 제외하는 쿼리

    private List<Member> searchMember1(String usernameParam, Integer ageParam) {

        BooleanBuilder booleanBuilder = new BooleanBuilder();

        if (usernameParam != null) {
            booleanBuilder.and(member.username.eq(usernameParam));
        }

        if (ageParam != null) {
            booleanBuilder.and(member.age.eq(ageParam));
        }

        return queryFactory
                .selectFrom(member)
                .where(booleanBuilder)
                .fetch();
    }
    private List<Member> searchMember2(String usernameParam, Integer ageParam) {
        return queryFactory
                .selectFrom(member)
                .where(usernameEq(usernameParam), ageEq(ageParam))
                .fetch();
    }

    private Predicate ageEq(Integer ageParam) {
        if (ageParam == null) {
            return null;
        }
        return member.age.eq(ageParam);
    }

    private Predicate usernameEq(String usernameParam) {
        if (usernameParam == null) {
            return null;
        }
        return member.username.eq(usernameParam);
    }

여기서는 조금 이해가 되었다.

QueryDSL을 사용하지 않는다면

  1. findByName, findByAge, findByNameAndAge 3가지를 따로 사용해야한다
  2. 그리고 로직이 복잡해져서 이 쿼리를 중첩해서 사용해야한다면?

1번은 코드가 안이쁘다 정도이지만
2번은 코드를 관리하기 정말 힘들 것 같다.

현재 프로젝트에서 사용, 소감

현재 사용중인 JPQL 코드

        reader.setQueryString("SELECT r FROM Restaurant r " +
                                      "WHERE r.openDataInformation.fullAddress LIKE :address" +
                                      " AND r.crawlComplete = false" +
                                      " AND r.restaurantId NOT IN (SELECT f.recordDataId FROM FailedRecord f)");
        reader.setParameterValues(Map.of("address", "%마포구%"));
    @Query("SELECT s.restaurantId " +
            "FROM UserRestaurantScrap s " +
            "WHERE s.userId = :userId " +
            "ORDER BY s.createdAt DESC")
    List<String> findAllRestaurantIdByUserId(String userId);
        @Query("SELECT r " +
            "FROM RestaurantNaverReviewFeatureCount r " +
            "JOIN FETCH r.naverReviewFeature " +
            "WHERE r.restaurantId = :restaurantId " +
            "ORDER BY r.reviewCount DESC " +
            "LIMIT 2")
    List<RestaurantNaverReviewFeatureCount> findTop2ByRestaurantIdOrderByReviewCountDesc(String restaurantId);

복잡한건 이정도이다.

결론

코드 유지보수 측면에서 좋은것 보다도
쿼리 오류를 런타임에러가 아닌 컴파일 에러로 잡을 수 있는게 가장 큰 장점인 것 같다.

https://github.com/team2-yumst/yumst/issues/31
현재 진행중인 프로젝트에서도 쿼리 관련 버그 수정을 한 적이 있는데
이런 경우 없이 안심? 할수 있는 점이 좋은 것 같다.

내 생각에 현재는 필요 없다.
하지만 쿼리 최적화를 계획하고 있는데,
그때 필요하다면 사용하기로.

summary 카테고리의 다른 글

Categories:

Updated: