[Jpa] QueryDSL

QueryDSL

정적 타입을 사용해 JPQL을 생성할 수 있는 프레임워크이다. JPA를 사용하여 여러 조건에 해당하는 조회 쿼리를 작성하려면 findByIdAndNameAnd… 등의 메서드를 생성하거나 @Query 어노테이션을 사용하여 직접 JPQL을 작성하여야 한다.

메서드 방식은 동적으로 조회 쿼리를 생성하는데 어려움이 있고, JPQL은 문자열을 기반으로 하기 떄문에 런타임 시점에 쿼리가 잘못되었는지 알 수 있음.

QueryDSL은 Q클래스를 사용해 타입에 안정적인 방식으로 쿼리를 동적으로 생성할 수 있다.

환경

의존성

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    runtimeOnly 'com.h2database:h2'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'

    compile("com.querydsl:querydsl-core")
    compile("com.querydsl:querydsl-jpa")
    annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jpa"
    annotationProcessor("jakarta.persistence:jakarta.persistence-api")
    annotationProcessor("jakarta.annotation:jakarta.annotation-api")
}

아직 구체적으로 어떤 의존성인지 모르겠다.

1. Entity 생성

@Getter
@NoArgsConstructor
@Entity
public class Person {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;
    private String name;

    @Builder
    public Person(String name) {
        this.name = name;
    }
}

2. QClass 생성을 위한 gradle task 작성


def generated='src/main/generated'
sourceSets {
    main.java.srcDirs += [ generated ]
}

tasks.withType(JavaCompile) {
    options.annotationProcessorGeneratedSourcesDirectory = file(generated)
}

clean.doLast {
    file(generated).deleteDir()
}

3. Gradle Task에 other하위에 있는 compileJava를 실행

src/main/generated 디렉토리에 QPerson 클래스가 생성된다.

@Generated("com.querydsl.codegen.EntitySerializer")
public class QPerson extends EntityPathBase<Person> {

    private static final long serialVersionUID = 2074619141L;

    public static final QPerson person = new QPerson("person");

    public final NumberPath<Long> id = createNumber("id", Long.class);

    public final StringPath name = createString("name");

    public QPerson(String variable) {
        super(Person.class, forVariable(variable));
    }

    public QPerson(Path<? extends Person> path) {
        super(path.getType(), path.getMetadata());
    }

    public QPerson(PathMetadata metadata) {
        super(Person.class, metadata);
    }

}

4. QueryDSL Config파일 작성

@Configuration
public class QuerydslConfiguration {
    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

5. 조회용 QueryDSL Repository 작성

import static com.zkdlu.querydsl.domain.QPerson.person;

@RequiredArgsConstructor
@Repository
public class PersonQueryRepository {
    private final JPAQueryFactory queryFactory;

    public List<Person> findByName(String name) {
        return queryFactory.selectFrom(person)
                .where(person.name.eq(name))
                .fetch();
    }
}

6. 테스트

@ExtendWith(SpringExtension.class)
@SpringBootTest
class PersonQueryRepositoryTest {
    @Autowired
    private PersonRepository personRepository;
    @Autowired
    private PersonQueryRepository personQueryRepository;

    @Test
    @DisplayName("QueryDSL 테스트")
    public void queryDsl_Test() {
        //given
        String name = "geon";
        personRepository.save(new Person(name));

        //when
        List<Person> result = personQueryRepository.findByName(name);

        //then
        assertThat(result.size()).isEqualTo(1);
    }
}

조회 로그

Hibernate: select person0_.id as id1_0_, person0_.name as name2_0_ from person person0_ where person0_.name=?

동적 조회 조건 생성하기

QueryDSL의 BooleanExpression을 사용하면 동적으로 Where 조건을 생성할 수 있다.

QueryDSL의 where 조건에 ,(쉼표) 로 구분하여 파라미터를 넘겨주면 And와 함께 조건을 생성하고 만약 파라미터로 null이 올 경우 조건에서 제외를 한다.

import static com.zkdlu.querydsl.domain.QPerson.person;

@RequiredArgsConstructor
@Repository
public class PersonQueryRepository {
    private final JPAQueryFactory queryFactory;

    public List<Person> findDynamic(String id, String name) {
        return queryFactory.selectFrom(person)
                .where(equalId(id)), 
                        equalName(name))
                .fetch();
    }
    
    private BooleanExpression equalId(String id) {
        if (StringUtils.isEmpty(id)) {
            return null;
        }
        return person.id.eq(id);
    }
    
    private BooleanExpression equalName(String name) {
        if (StringUtils.isEmpty(name)) {
            return null;
        }
        return person.name.eq(name);
    }
}

BooleanBuiler를 이용한 and(), or() 메서드를 사용하여 조건들을 추가할 수 도 있다.