Introduction to Querydsl

Querydsl is a framework that enables type-safe creation and management of HQL (Hibernate Query Language) queries. That is the official definition. A simpler interpretation is: “Querydsl lets you write queries in Java code.”


Why Querydsl?

Assume you use JPA. For simple queries, interface method signatures are usually enough. For example, a method like this to fetch articles whose title contains a specific string:

Article findByTitleContains(String title);

What about more complex queries? Instead of only filtering by title text, you now filter by the level of the user who wrote the article.

In that case, JPA built-in method naming is often insufficient, so you may consider native SQL. Below is a method that fetches articles written by users above a specific level.

@Query(value = "SELECT id, title, user_id FROM article WHERE user_id IN (SELECT id FROM user WHERE level > :level)", nativeQuery = true)
List<Article> findByLevel(String level);

Look at this native query again. Aside from readability, manually building query strings is error-prone.

Then what does this look like in Querydsl? Even before detailed usage, here is the equivalent Querydsl example.

public List<Article> findByUserLevel(String level) {
    QArticle article = QArticle.article;
    QUser user = QUser.user;

    return queryFactory.selectFrom(article)
        .where(
            article.userId.in(
                JPAExpressions
                    .select(user.id)
                    .from(user)
                    .where(user.level.gt(level))
            )
        )
        .fetch();
}

It is much more readable than the native query above. Even if total code volume increases.

Also, if you pass a parameter with wrong type, compilation fails and catches potential bugs earlier. In other words, query parameter type errors are detectable before runtime.

Now let’s move to actual Querydsl setup and usage.


Querydsl setup

This example was rewritten as of July 2021. Full source code is linked at the end.

Versions used in this example:

  • Spring Boot: 2.5.0
  • Gradle 7.1.1
  • Querydsl: 4.4.0
  • Lombok: 1.18.18

Gradle settings

First, declare dependencies in build.gradle. Some unrelated dependencies are omitted; only Querydsl-related items are listed.

// ... omitted

dependencies {
    // ... omitted
    
    implementation "com.querydsl:querydsl-core:${queryDslVersion}"
    implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"

    /*
     * Required to address `NoClassDefFoundError`.
     * Note: name changed from javax -> jakarta.
     */
    annotationProcessor(
            "jakarta.persistence:jakarta.persistence-api",
            "jakarta.annotation:jakarta.annotation-api",
            "com.querydsl:querydsl-apt:${queryDslVersion}:jpa")
}

sourceSets {
    main {
        java {
            srcDirs = ["$projectDir/src/main/java", "$projectDir/build/generated"]
        }
    }
}

// ... omitted


Querydsl Config

Next, configure Querydsl. The jpaQueryFactory bean defined here is used in repositories.

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.querydsl.jpa.impl.JPAQueryFactory;

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

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


How to use Querydsl

Setup is complete. Now let’s use Querydsl.

Define entity classes

Define entity classes mapped to article and user tables.

@Getter
@Entity
@Table(name = "article")
public class Article {
	@Id
	private Integer id;
	@Column(name = "user_id")
	private Integer userId;
	private String title;
}
@Getter
@Entity
@Table(name = "user")
public class User {
	@Id
	private Integer id;
	private String name;
	private String level;
}


Generate Q classes

During compilation, Querydsl generates Q classes from entities. Queries are written based on those generated classes.

To generate Q classes, compile source via Gradle. Run build task (which includes compile) or run compileJava if your goal is only Q-class generation.

After execution, Q classes appear in build output like below.

q class



Define repository

Next, create repository layers where queries are written/executed. Both JPA interface methods and Querydsl methods are used.

First, define signatures for Querydsl methods. The naming follows ~RepositoryCustom.

/**
 * Declare signatures for Querydsl queries here,
 * then implement in `~RepositoryImpl`.
 */
public interface ArticleRepositoryCustom {
	List<Article> findByLevelUsingQuerydsl(String level);
}

Then implement behavior using those signatures. Use JPAQueryFactory from QuerydslConfig to build/execute query. Method suffix ~UsingQuerydsl is arbitrary; in team projects, follow your convention.

/**
 * Query implementation using Querydsl.
 */
@Repository
@RequiredArgsConstructor
public class ArticleRepositoryImpl implements ArticleRepositoryCustom {

	private final JPAQueryFactory queryFactory;

	public List<Article> findByLevelUsingQuerydsl(String level) {
		// use Q classes
		QArticle article = QArticle.article;
		QUser user = QUser.user;

		return queryFactory.selectFrom(article)
			.where(
				article.userId.in(
					JPAExpressions
						.select(user.id)
						.from(user)
						.where(user.level.gt(level))
				)
			)
			.fetch();
	}
}

Custom repositories must follow naming conventions. Without extra settings, suffix ~Impl is required for Spring to find implementation. You can verify this from internal code in spring-data classes RepositoryConfigurationSourceSupport and AnnotationRepositoryConfigurationSource.

Finally, define the repository interface to use JPA methods together. findByLevel below is added for comparison with Querydsl.

public interface ArticleRepository extends JpaRepository<Article, Integer>, ArticleRepositoryCustom {
	@Query(value = "SELECT * FROM article WHERE user_id IN (SELECT id FROM user WHERE level > :level)", nativeQuery = true)
	List<Article> findByLevel(String level);
}

Test

Now write a simple test to run the code.

@SpringBootTest
class ExampleApplicationTests {

	@Autowired
	private ArticleRepository articleRepository;

	@Test
	void testGetArticleList() {
		// Native Query
		List<Article> articleList = articleRepository.findByLevel("1");

		System.out.println("--------------------------------------------------");
		
		// Querydsl
		List<Article> articleListByQuerydsl = articleRepository.findByLevelUsingQuerydsl("1");

		Assertions.assertEquals(articleList.size(), articleListByQuerydsl.size());
	}
}

Add SQL logging settings to verify generated Querydsl SQL.

spring.jpa.properties.hibernate.show_sql=true # print executed SQL
spring.jpa.properties.hibernate.format_sql=true # pretty format SQL

Run the test. Printed queries show both queries are equivalent in result.

Hibernate: 
    SELECT
        id,
        title,
        user_id 
    FROM
        article 
    WHERE
        user_id IN (
            SELECT
                id 
            FROM
                user 
            WHERE
                level > ?
        )
--------------------------------------------------
Hibernate: 
    select
        article0_.id as id1_0_,
        article0_.title as title2_0_,
        article0_.user_id as user_id3_0_ 
    from
        article article0_ 
    where
        article0_.user_id in (
            select
                user1_.id 
            from
                user user1_ 
            where
                user1_.level>?
        )


Dynamic query

Another strong point of Querydsl is dynamic query composition. You can define conditional expressions in code as below. If a parameter is absent and null reaches where, that condition is skipped.

public List<Article> searchArticle(String title, Integer userId) {
    return queryFactory.selectFrom(article)
        .where(titleContains(title), userIdEq(userId))
        .fetch();
}

private BooleanExpression titleContains(String title) {
    return StringUtils.isNotBlank(title) ? article.title.contains(title) : null;
}

private BooleanExpression userIdEq(Integer userId) {
    return userId != null ? article.userId.eq(userId) : null;
}


Closing

When using JPA, native queries appear when built-in features are not enough. But as shown above, native query strings are easy to mistype and harder to read.

With Querydsl, you use IDE autocomplete and catch type/syntax issues at compile time. Dynamic query handling is also straightforward. Applicability still depends on project context, so choose based on your project needs.


Example source code

All source code in this post is in the repository below.