In the Java ecosystem, querydsl is one of the most popular strongly-typed query frameworks, often used with JPA/Hibernate. This article compares these two frameworks across multiple dimensions, exploring the evolution of strongly-typed query DSLs.
Basic Query Comparison
Given a User entity, query users whose name contains "test".
querydsl
QUser user = QUser.user;
List<User> users = queryFactory
.selectFrom(user)
.where(user.name.like("%test%"))
.fetch();
easy-query
List<User> users = entityQuery.queryable(User.class)
.where(u -> u.name().like("test"))
.toList();
Differences:
- querydsl requires pre-defining a
QUservariable - easy-query uses Lambda, no extra variables needed, and
likeauto-adds%wildcards
Join Query Comparison
Scenario: Query users where the company name contains "Tech".
querydsl
QUser user = QUser.user;
QCompany company = QCompany.company;
List<User> users = queryFactory
.selectFrom(user)
.join(user.company, company)
.where(company.name.like("%Tech%"))
.fetch();
easy-query
List<User> users = entityQuery.queryable(User.class)
.where(u -> u.company().name().like("Tech"))
.toList();
Differences:
- querydsl requires explicit join declaration and related Q-classes
- easy-query uses navigation properties for implicit join —
u.company().name()automatically generates the JOIN clause
The generated SQL is identical, but easy-query's code is more concise and closer to natural language.
Subquery Comparison
Scenario: Query users who have comments.
querydsl
QUser user = QUser.user;
QComment comment = QComment.comment;
List<User> users = queryFactory
.selectFrom(user)
.where(JPAExpressions
.selectOne()
.from(comment)
.where(comment.userId.eq(user.id))
.exists())
.fetch();
easy-query
List<User> users = entityQuery.queryable(User.class)
.where(u -> u.comments().any())
.toList();
Differences:
- querydsl requires
JPAExpressionsto manually construct subqueries - easy-query generates EXISTS subquery implicitly via
comments().any()
Aggregate Subquery Comparison
Scenario: Query users with more than 10 comments.
querydsl
QUser user = QUser.user;
QComment comment = QComment.comment;
List<User> users = queryFactory
.selectFrom(user)
.where(JPAExpressions
.select(comment.count())
.from(comment)
.where(comment.userId.eq(user.id))
.gt(10L))
.fetch();
easy-query
List<User> users = entityQuery.queryable(User.class)
.where(u -> u.comments().count().gt(10L))
.toList();
Differences:
- querydsl requires a complete subquery expression
- easy-query does it in one line:
u.comments().count().gt(10L)
Dynamic Query Comparison
Scenario: Dynamically build query conditions based on frontend parameters.
querydsl
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(name)) {
builder.and(user.name.like("%" + name + "%"));
}
if (minAge != null) {
builder.and(user.age.goe(minAge));
}
if (companyName != null) {
QCompany company = QCompany.company;
// Need to add join externally
builder.and(company.name.eq(companyName));
}
List<User> users = queryFactory
.selectFrom(user)
.join(user.company, company) // Must explicitly join
.where(builder)
.fetch();
easy-query
List<User> users = entityQuery.queryable(User.class)
.where(u -> {
u.name().like(name); // Auto-skips if empty
u.age().ge(minAge); // Auto-skips if null
u.company().name().eq(companyName); // No join if condition inactive
})
.toList();
Differences:
- querydsl requires
BooleanBuilderfor manual concatenation, and joins must be declared explicitly (even if conditions don't apply) - easy-query auto-skips null/empty conditions, implicit join doesn't generate JOIN when condition is inactive
Complex Aggregation Comparison
Scenario: Count users and average age per department.
querydsl
QUser user = QUser.user;
List<Tuple> result = queryFactory
.select(user.department, user.count(), user.age.avg())
.from(user)
.groupBy(user.department)
.fetch();
// Manual Tuple handling required
for (Tuple tuple : result) {
String dept = tuple.get(user.department);
Long count = tuple.get(user.count());
Double avgAge = tuple.get(user.age.avg());
}
easy-query
List<Draft3<String, Long, BigDecimal>> result = entityQuery.queryable(User.class)
.groupBy(u -> GroupBy.of(u.department()))
.select(u -> Select.DRAFT.of(
u.key1(),
u.count(),
u.age().avg()
))
.toList();
// Strongly-typed access
for (var row : result) {
String dept = row.getValue1();
Long count = row.getValue2();
BigDecimal avgAge = row.getValue3();
}
Differences:
- querydsl returns
Tuple, requiring manual extraction via expressions - easy-query returns strongly-typed
Draftwith compile-time type checking
Conditional Aggregation Comparison
Scenario: Count users in Department A and Department B separately.
querydsl
QUser user = QUser.user;
List<Tuple> result = queryFactory
.select(
new CaseBuilder()
.when(user.department.eq("A")).then(1L).otherwise(0L).sum(),
new CaseBuilder()
.when(user.department.eq("B")).then(1L).otherwise(0L).sum()
)
.from(user)
.fetch();
easy-query
List<Draft2<Long, Long>> result = entityQuery.queryable(User.class)
.select(u -> Select.DRAFT.of(
u.id().count().filter(() -> u.department().eq("A")),
u.id().count().filter(() -> u.department().eq("B"))
))
.toList();
Differences:
- querydsl requires manually building
CASE WHENexpressions - easy-query uses
.filter()syntax sugar for clarity
Related Data Loading Comparison
Scenario: Query users and load their roles.
querydsl + JPA
// Option 1: EntityGraph
@EntityGraph(attributePaths = {"roles"})
List<User> findAll();
// Option 2: Fetch Join
List<User> users = queryFactory
.selectFrom(user)
.leftJoin(user.roles).fetchJoin()
.fetch();
// Problem: Multiple collection fetch joins cause Cartesian products
easy-query
List<User> users = entityQuery.queryable(User.class)
.include(u -> u.roles())
.toList();
// Supports multiple includes without Cartesian products
List<User> users = entityQuery.queryable(User.class)
.include(u -> u.roles())
.include(u -> u.posts())
.toList();
// Supports deep nesting
List<User> users = entityQuery.queryable(User.class)
.include2((c, u) -> {
c.query(u.roles());
c.query(u.posts().flatElement().comments());
})
.toList();
Differences:
- JPA's fetch join with multiple collections causes Cartesian product issues
- easy-query's include uses separate queries, avoiding Cartesian products
DTO Projection Comparison
Scenario: Query users and return a DTO that includes associated roles.
querydsl + JPA
// 1. Define DTO (no nested associations supported)
public record UserDTO(String id, String name) {}
List<UserDTO> users = queryFactory
.select(Projections.constructor(UserDTO.class, user.id, user.name))
.from(user)
.fetch();
// To get related data, need separate queries and manual assembly
easy-query
@Data
public class UserDTO {
private String id;
private String name;
@Navigate(value = RelationTypeEnum.ManyToMany)
private List<RoleDTO> roles; // Supports nested associations
}
List<UserDTO> users = entityQuery.queryable(User.class)
.selectAutoInclude(UserDTO.class)
.toList();
// Roles auto-populated
Differences:
- querydsl projections don't support nested associations
- easy-query's
selectAutoIncludeauto-populates navigation properties in DTOs
Feature Comparison Table
| Feature | querydsl | easy-query |
|---|---|---|
| Strongly-typed queries | ✅ | ✅ |
| Code generation | APT (Q-class) | APT (Proxy) |
| Lambda syntax | ❌ | ✅ |
| Implicit Join | ❌ | ✅ |
| Implicit Subquery | ❌ | ✅ |
| Dynamic condition skip | Manual | Automatic |
| Conditional aggregation sugar | ❌ | ✅ .filter()
|
| Multi-collection Include | Cartesian product | ✅ Separate queries |
| DTO nested associations | ❌ | ✅ selectAutoInclude
|
| Subquery optimization | ❌ | ✅ GROUP JOIN |
| JPA dependency | ✅ | ❌ Standalone |
Summary
querydsl pioneered strongly-typed queries in Java, solving the pain of string-based SQL concatenation. But it remains "explicit" — you must explicitly declare Q-classes, explicitly join, and explicitly construct subqueries.
easy-query takes it further with navigation properties and implicit inference, making code closer to business semantics:
-
u.company().name().like("Tech")instead ofjoin(user.company, company).where(company.name.like(...)) -
u.comments().any()instead ofJPAExpressions.selectOne().from(comment).where(...).exists() -
u.comments().count().gt(10)instead ofJPAExpressions.select(comment.count()).from(comment).where(...).gt(10)
You write business logic; the framework generates optimal SQL.
Links
- easy-query GitHub: https://github.com/dromara/easy-query
- easy-query Documentation: https://www.easy-query.com/easy-query-doc/en
- querydsl: http://querydsl.com/
Top comments (0)