easy-query vs Spring Data JPA: Even with Querydsl and Hibernate, the Gap Is Bigger Than It Looks
If you compare easy-query only with plain Spring Data JPA, the result is too easy to dismiss.
That is not how most real Java teams build query layers anymore.
Once queries become serious, Spring Data JPA is usually paired with:
-
Querydsl, for type-safe predicates and dynamic filtering -
Hibernate, for the actual ORM runtime, entity loading, and association fetching
So the real comparison is not:
-
easy-queryvsSpring Data JPA
It is much closer to:
-
easy-queryvsSpring Data JPA + Querydsl + Hibernate
And that is exactly where the difference becomes interesting.
Because even after adding the two strongest common “fixes” on the Spring side, easy-query still feels more coherent at the business-query layer, especially when the discussion moves beyond simple predicates and into relation navigation, nested DTOs, and object graph assembly.
In this article, I will look at three practical questions:
- Which side offers the better type-safe writing model?
- Which DSL holds up better as queries become more relational and dynamic?
- Which side handles DTOs and object graphs with less friction?
1. These Are Not Two Symmetric Systems
Scenario: a project needs an ORM layer, and queries will grow more complex over time
A typical Spring Data JPA entry point looks like this:
public interface UserRepository extends JpaRepository<User, Long>,
JpaSpecificationExecutor<User>,
QuerydslPredicateExecutor<User> {
}
In practice, query capabilities are spread across multiple mechanisms:
- derived query methods
@QuerySpecificationQuerydslPredicateExecutor- projections
EntityGraph
With easy-query, the entry point is usually the query DSL itself:
List<User> users = easyQuery.queryable(User.class)
.where(u -> u.name().like("test"))
.toList();
Takeaway
Spring Data JPA is primarily a repository abstraction and query access layer.
Querydsl strengthens its type-safe query writing.
Hibernate provides the actual ORM runtime.
easy-query, by contrast, keeps query expression, relation navigation, DTO mapping, and object graph assembly in one framework model.
That is why these two options may both be called “ORM” in projects, while still competing at very different levels.
2. Type-Safe Writing: Querydsl Improves Spring Data JPA, but It Does Not Complete It
Scenario: find users whose name contains test and age is greater than 18
Spring Data JPA + Querydsl:
QUser user = QUser.user;
List<User> users = queryFactory
.selectFrom(user)
.where(
user.name.contains("test"),
user.age.gt(18)
)
.fetch();
easy-query:
List<User> users = easyQuery.queryable(User.class)
.where(u -> {
u.name().like("test");
u.age().gt(18);
})
.toList();
Takeaway
Querydsl definitely upgrades Spring Data JPA beyond string-based queries and verbose Criteria code.
For field-level predicates, it is already a mature type-safe solution.
But what it improves is mainly predicate writing, not the entire query model.
In other words, Spring Data JPA + Querydsl mainly solves “how to write the where clause nicely,” while easy-query tries to solve “how to keep the entire business query model coherent.”
3. Dynamic Queries: Better Than Specification, Still Centered on Predicate Assembly
Scenario: backend search page with optional filters
Spring Data JPA + Querydsl:
QUser user = QUser.user;
BooleanBuilder builder = new BooleanBuilder();
if (name != null && !name.isEmpty()) {
builder.and(user.name.contains(name));
}
if (minAge != null) {
builder.and(user.age.goe(minAge));
}
List<User> users = queryFactory
.selectFrom(user)
.where(builder)
.fetch();
easy-query:
List<User> users = easyQuery.queryable(User.class)
.filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
.where(u -> {
u.name().like(name);
u.age().ge(minAge);
})
.toList();
Takeaway
Once Querydsl is added, the dynamic query experience is indeed much better than Specification.
But the core model is still manual predicate assembly.
easy-query keeps dynamic conditions inside the DSL lifecycle itself.
The practical difference is not whether both can handle dynamic queries. Both can. The difference is whether the code stays as a query language, or slowly degrades back into a predicate builder.
4. Relation Queries: Querydsl Helps a Lot, but It Still Thinks in Joins
Scenario: find users whose company name contains Tech
Spring Data JPA + Querydsl:
QUser user = QUser.user;
QCompany company = QCompany.company;
List<User> users = queryFactory
.selectFrom(user)
.leftJoin(user.company, company)
.where(company.name.contains("Tech"))
.fetch();
easy-query:
List<User> users = easyQuery.queryable(User.class)
.where(u -> u.company().name().like("Tech"))
.toList();
Takeaway
Querydsl gives Spring Data JPA a very solid type-safe join API.
But its core expression model is still explicit join composition.
easy-query starts from relation navigation instead.
In simple to-one cases this mostly feels like a style difference. In to-many, dynamic relation filtering, and graph assembly, it becomes a deeper architectural difference.
5. To-Many Queries: Querydsl Is Cleaner Than JPQL, but Still Exposes Query Structure
Scenario: find users who have comments
Spring Data JPA + Querydsl:
QUser user = QUser.user;
QComment comment = QComment.comment;
List<User> users = queryFactory
.selectFrom(user)
.where(
JPAExpressions.selectOne()
.from(comment)
.where(comment.user.eq(user))
.exists()
)
.fetch();
easy-query:
List<User> users = easyQuery.queryable(User.class)
.where(u -> u.comments().any())
.toList();
Scenario: find users with more than 10 comments
Spring Data JPA + Querydsl:
QUser user = QUser.user;
QComment comment = QComment.comment;
List<User> users = queryFactory
.selectFrom(user)
.where(
JPAExpressions.select(comment.count())
.from(comment)
.where(comment.user.eq(user))
.gt(10L)
)
.fetch();
easy-query:
List<User> users = easyQuery.queryable(User.class)
.where(u -> u.comments().count().gt(10L))
.toList();
Takeaway
Querydsl makes to-many subqueries much clearer than raw JPQL.
But developers still have to express the subquery shape directly.
easy-query lets them continue writing in collection semantics.
So Querydsl improves the readability of JPQL-style structure. easy-query improves the readability of business relation semantics.
6. Dynamic Relation Filters: This Is Still an easy-query Advantage
Scenario: filter users by optional company name, and avoid unnecessary relation generation when the value is empty
Spring Data JPA + Querydsl:
QUser user = QUser.user;
QCompany company = QCompany.company;
JPAQuery<User> query = queryFactory.selectFrom(user);
if (companyName != null && !companyName.isEmpty()) {
query.leftJoin(user.company, company)
.where(company.name.eq(companyName));
}
List<User> users = query.fetch();
easy-query:
List<User> users = easyQuery.queryable(User.class)
.filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
.where(u -> {
u.company().name().eq(companyName);
})
.toList();
Takeaway
Querydsl can express joins and predicates elegantly, but developers still decide explicitly when a join should exist.
easy-query can place value filtering and relation-path activation inside one DSL rule system.
For search-heavy business UIs, this leads to noticeably more compact query code.
7. Object Mapping, Part One: Querydsl Improves Projection, but Not Object Graphs
Scenario: flat DTO projection
Spring Data JPA + Querydsl:
QUser user = QUser.user;
List<UserDTO> users = queryFactory
.select(Projections.constructor(UserDTO.class, user.id, user.name, user.age))
.from(user)
.fetch();
Or:
List<UserDTO> users = queryFactory
.select(Projections.bean(UserDTO.class,
user.id,
user.name,
user.age))
.from(user)
.fetch();
easy-query:
List<UserDTO> users = easyQuery.queryable(User.class)
.select(UserDTO.class)
.toList();
Takeaway
Querydsl projections are mature and useful.
They clearly improve Spring Data JPA's projection story.
But they still solve “how to map result rows into DTOs,” not “how the DTO shape should influence query planning.”
That is why Querydsl is still best understood as a projection tool, not an object graph assembly model.
8. Nested DTOs: Querydsl Can Do It, but the Weight Rises Quickly
Scenario: map company data into UserDTO.company
With Spring Data JPA + Querydsl, teams usually end up with one of these:
- flatten the result and assemble manually
- use
Projections.bean/fields/constructor - combine aliases and custom mapping logic
Once the target shape becomes something like:
public class UserDTO {
private Long id;
private String name;
private CompanyDTO company;
}
the query code starts to get heavier very quickly, especially when nested objects, aliases, and collections are added.
easy-query can bind paths directly into the target type:
@Data
public class TopicSelfVO {
private String id;
private String name;
private static final MappingPath UNAME1_PATH = TestSelfProxy.TABLE.myUser().name();
@NavigateJoin(pathAlias = "UNAME1_PATH")
private String uname1;
private static final MappingPath UNAME2_PATH = TestSelfProxy.TABLE.parent().myUser().name();
@NavigateJoin(pathAlias = "UNAME2_PATH")
private String uname2;
}
Query:
List<TopicSelfVO> list = easyQuery.queryable(TestSelf.class)
.selectAutoInclude(TopicSelfVO.class)
.toList();
Takeaway
Querydsl can support nested DTO scenarios, but it often feels like assembling structure from projection primitives.
easy-query lets the target type participate directly in path binding and result planning.
That is why the former feels like a projection framework, while the latter behaves more like an object-graph-aware query framework.
9. Object Graph Assembly: Querydsl Still Does Not Solve This Core Weakness
Scenario: fetch users with bank cards and banks
In Spring Data JPA + Querydsl, the usual combination is:
join fetchEntityGraphProjections- manual post-processing
Example:
@EntityGraph(attributePaths = {"bankCards", "bankCards.bank"})
List<User> findByNameContaining(String name);
Or with Querydsl:
QUser user = QUser.user;
QBankCard card = QBankCard.bankCard;
QBank bank = QBank.bank;
List<Tuple> rows = queryFactory
.select(user.id, user.name, card.id, card.code, bank.name)
.from(user)
.leftJoin(user.bankCards, card)
.leftJoin(card.bank, bank)
.fetch();
Then manual assembly:
Map<Long, UserDTO> result = new LinkedHashMap<>();
// assemble rows into an object graph
Querydsl does offer GroupBy:
query.transform(
GroupBy.groupBy(user.id)
.as(GroupBy.list(Projections.tuple(card.id, card.code)))
);
But this is still result grouping and transformation. It is not the same as the framework natively understanding a DTO object graph and assembling it for you.
easy-query:
List<UserDTO> users = easyQuery.queryable(User.class)
.selectAutoInclude(UserDTO.class)
.toList();
DTO:
@Data
public class SysUserDTO {
private String id;
private String name;
@Navigate(RelationTypeEnum.OneToMany)
private List<InternalBankCards> bankCards;
@Data
public static class InternalBankCards {
private static final ExtraAutoIncludeConfigure EXTRA_AUTO_INCLUDE_CONFIGURE =
SysBankCardProxy.TABLE.EXTRA_AUTO_INCLUDE_CONFIGURE().where(card -> {
card.type().eq("savings");
});
private String id;
private String code;
@Navigate(RelationTypeEnum.ManyToOne)
@ForeignKey
private InternalBank bank;
}
}
Takeaway
EntityGraph solves entity fetching.
Querydsl Projections solve result projection.
GroupBy solves grouped result transformation.
Even together, they still do not amount to a native object graph assembly model.
That is exactly the area where easy-query is strongest.
10. Hibernate Is Strong, but Not in Business Query DSL Design
This is also worth stating clearly.
Hibernate is extremely strong at ORM infrastructure:
- persistence context
- first-level cache
- entity state management
- lazy loading
- HQL / Criteria / SQM
- association fetching
- entity graphs and fetch profiles
But it has never been especially strong at business-query-oriented DSL design or complex DTO/object graph assembly.
Even traditional result conversion APIs like:
query.setTupleTransformer(Transformers.aliasToBean(UserDTO.class));
are still fundamentally flat alias-to-property mapping.
That helps with result conversion, but it does not naturally solve:
- nested DTOs
- nested collection DTOs
- DTO-structure-driven queries
- dynamic object graph assembly
So even after adding Hibernate into the comparison, the conclusion barely changes: it strengthens ORM infrastructure, not business query language design.
11. Put the Three Pieces Together, and the Trade-Off Is Obvious
What Spring Data JPA + Querydsl + Hibernate does well:
- consistent repository access
- mature type-safe predicate writing
- complete JPA/Hibernate ecosystem
- strong CRUD, paging, sorting, and transaction integration
- low organizational learning cost
Where it still falls short:
- query capability is fragmented across Repository, JPQL, Specification, Querydsl, and EntityGraph
- complex relation queries often require switching between several styles
- Querydsl improves type-safe expression, but does not unify DTO/object graph assembly
- EntityGraph is strong for entity loading, not for structured DTO returns
- complex business queries still often end with manual assembly
What easy-query gets right is the opposite:
- unified type-safe expression
- unified relation navigation
- unified dynamic filtering
- unified DTO projection and object graph assembly
That is why, from a business query framework perspective, easy-query already feels more complete.
Final Thoughts
If the question is only whether both can be used as ORM solutions, the answer is yes.
But if the question is which one is better suited for complex business querying, then looking at Spring Data JPA alone is not enough. You really need to consider the real-world stack:
Spring Data JPAQuerydslHibernate
Even under that more realistic comparison, the picture remains clear.
For type-safe writing
- Querydsl already brings Spring Data JPA to a solid level
- but easy-query is still closer to a type-safe business-query DSL, not just a type-safe predicate API
For DSL capability
-
Spring Data JPA + Querydslis closer to type-safe JPQL / JPA query writing -
easy-queryis closer to a language for entity relations and collection semantics
For object mapping
-
Spring Data JPA + Querydsl + Hibernatecan handle projection, grouping, entity fetching, and result conversion - but it still lacks a unified native solution for complex DTO and object graph assembly
- easy-query is clearly more complete here
So the practical conclusion is:
Spring Data JPA + Querydsl + Hibernate is a mature but assembled query stack; easy-query is closer to an integrated ORM/query DSL built for business querying.
If your project is mainly about persistence with medium-complexity queries, the former remains a solid choice.
If your project is mainly about complex business queries, relation navigation, and object graph assembly, easy-query currently offers the stronger model.
Top comments (0)