DEV Community

Li
Li

Posted on

easy-query vs Spring Data JPA

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-query vs Spring Data JPA

It is much closer to:

  • easy-query vs Spring 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:

  1. Which side offers the better type-safe writing model?
  2. Which DSL holds up better as queries become more relational and dynamic?
  3. 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> {
}
Enter fullscreen mode Exit fullscreen mode

In practice, query capabilities are spread across multiple mechanisms:

  • derived query methods
  • @Query
  • Specification
  • QuerydslPredicateExecutor
  • 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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

easy-query:

List<User> users = easyQuery.queryable(User.class)
    .where(u -> {
        u.name().like("test");
        u.age().gt(18);
    })
    .toList();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

easy-query:

List<User> users = easyQuery.queryable(User.class)
    .where(u -> u.company().name().like("Tech"))
    .toList();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

easy-query:

List<User> users = easyQuery.queryable(User.class)
    .where(u -> u.comments().any())
    .toList();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

easy-query:

List<User> users = easyQuery.queryable(User.class)
    .where(u -> u.comments().count().gt(10L))
    .toList();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

easy-query:

List<User> users = easyQuery.queryable(User.class)
    .filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
    .where(u -> {
        u.company().name().eq(companyName);
    })
    .toList();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Or:

List<UserDTO> users = queryFactory
    .select(Projections.bean(UserDTO.class,
        user.id,
        user.name,
        user.age))
    .from(user)
    .fetch();
Enter fullscreen mode Exit fullscreen mode

easy-query:

List<UserDTO> users = easyQuery.queryable(User.class)
    .select(UserDTO.class)
    .toList();
Enter fullscreen mode Exit fullscreen mode

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:

  1. flatten the result and assemble manually
  2. use Projections.bean/fields/constructor
  3. 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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Query:

List<TopicSelfVO> list = easyQuery.queryable(TestSelf.class)
    .selectAutoInclude(TopicSelfVO.class)
    .toList();
Enter fullscreen mode Exit fullscreen mode

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:

  1. join fetch
  2. EntityGraph
  3. Projections
  4. manual post-processing

Example:

@EntityGraph(attributePaths = {"bankCards", "bankCards.bank"})
List<User> findByNameContaining(String name);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Then manual assembly:

Map<Long, UserDTO> result = new LinkedHashMap<>();
// assemble rows into an object graph
Enter fullscreen mode Exit fullscreen mode

Querydsl does offer GroupBy:

query.transform(
    GroupBy.groupBy(user.id)
        .as(GroupBy.list(Projections.tuple(card.id, card.code)))
);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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 JPA
  • Querydsl
  • Hibernate

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 + Querydsl is closer to type-safe JPQL / JPA query writing
  • easy-query is closer to a language for entity relations and collection semantics

For object mapping

  • Spring Data JPA + Querydsl + Hibernate can 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)