If you only look at easy-query as "another Java ORM", you will almost certainly underestimate it.
Java ORM has never really been one thing. Some teams live in Spring Data JPA, some add Querydsl when queries get harder, and some move toward jOOQ when SQL itself becomes the design center.
My conclusion is simple:
easy-query is not just trying to make querying nicer. It is trying to make Java business querying coherent.
That difference matters more than it sounds.
easy-query stands out because four things happen at the same time:
- Its type-safe DSL does not stop at field predicates.
- Its object mapping model does not stop at flat DTO projection.
- It has a surprising amount of practical functionality built in.
- Its implementation shows a lot of restraint around unnecessary runtime work.
This post is about those four points.
All examples below assume the common Spring Boot setup with easy-query-spring-boot-starter and an injected easyEntityQuery.
1. The real advantage is not "type safety"
A lot of Java libraries can claim some kind of type safety. That part is not rare anymore.
What is much rarer is this:
Does the DSL still hold up once a query moves from field predicates into object relations, collections, subqueries, and structured results?
That is where easy-query starts to separate itself.
Take the obvious case first:
List<User> users = easyEntityQuery.queryable(User.class)
.where(u -> {
u.name().like("jack");
u.age().ge(18);
})
.toList();
Nice, but not unusual.
Now look at relation navigation:
List<User> users = easyEntityQuery.queryable(User.class)
.where(u -> u.company().name().like("Tech"))
.toList();
And then to-many collection semantics:
List<User> users = easyEntityQuery.queryable(User.class)
.where(u -> {
u.bankCards().any(card -> card.type().eq("Savings"));
u.bankCards()
.where(card -> card.bank().name().eq("ICBC"))
.count()
.ge(2L);
})
.toList();
This is exactly where many Java query APIs collapse back into explicit exists, hand-written subqueries, count expressions, and manual predicate builders.
But in easy-query, the collection stays a collection, and the relation stays a relation.
That is the real shift: you are no longer writing a type-safe SQL fragment. You are writing something much closer to a business query language.
2. The "implicit" model is where things get interesting
One of the most distinctive parts of easy-query is its implicit query model.
This is not presented as one convenience method. It is treated as a whole family of capabilities:
- implicit join
- implicit subquery
- implicit group
- implicit partition
- implicit case when
That list tells you a lot about the framework's ambition. This is not just about hiding SQL. It is about letting higher-level business semantics survive longer inside the DSL.
More importantly, these are not abstract labels. They show up as very concrete query styles.
Implicit join
List<User> users = easyEntityQuery.queryable(User.class)
.where(u -> u.company().name().like("Tech"))
.toList();
The interesting part here is not that a join exists. It is that the query is still written as relation navigation, not as explicit join assembly.
Implicit subquery
List<SysUser> list = easyEntityQuery.queryable(SysUser.class)
.where(user -> {
user.bankCards().any(card -> card.type().eq("Savings"));
})
.toList();
This is the kind of condition that often becomes explicit exists logic in other Java stacks. Here it stays as collection semantics.
Implicit group
List<SysUser> list = easyEntityQuery.queryable(SysUser.class)
.subQueryToGroupJoin(u -> u.bankCards())
.where(user -> {
user.bankCards().where(card -> {
card.bank().name().eq("ICBC");
}).count().ge(2L);
user.bankCards().none(card -> {
card.bank().name().eq("CCB");
});
})
.toList();
This is where the model gets especially interesting. From the caller's perspective, the API still looks like relation-oriented business code. In practice, it can merge multiple subqueries into a grouped join style query and improve SQL performance when relation filters start piling up.
Implicit partition
List<SysUser> users = easyEntityQuery.queryable(SysUser.class)
.where(user -> {
user.bankCards()
.orderBy(card -> card.openTime().asc())
.elements(0, 1)
.none(card -> card.bank().name().eq("Hangzhou Bank"));
})
.toList();
This kind of expression is where partition-style thinking starts to appear naturally in the DSL, especially around top-N-per-group style relation queries.
Implicit case when
List<SysUser> users = easyEntityQuery.queryable(SysUser.class)
.where(user -> {
user.bankCards().where(card -> {
card.bank().name().eq("ICBC");
}).count().ge(2L);
})
.toList();
Once relation conditions and grouped aggregation start mixing, the framework is no longer just translating field predicates. It is already moving toward SQL constructs that typically end up as grouped expressions and case when-style logic underneath.
That matters because long-term ORM pain usually starts when the same relation is filtered three times, the same page accumulates multiple subqueries, and exists plus count conditions start spreading everywhere.
easy-query's answer is not "drop to raw SQL now". Its answer is "keep pushing the DSL downward."
3. It uses APT/KSP to generate helper code at compile time
easy-query uses APT / KSP to generate helper code at compile time.
The generated proxy types carry the actual DSL surface:
- a static
TABLEentry - typed column access like
score()orstatus() - field-name constants
- fetcher helpers
- select-as style binding support
That matters because it gives the DSL a concrete, typed object model instead of forcing everything through string paths or late reflection.
For example, a generated proxy looks like this in practice:
public class BlogEntityVO1Proxy extends AbstractProxyEntity<BlogEntityVO1Proxy, BlogEntityVO1> {
public static final BlogEntityVO1Proxy TABLE = createTable().createEmpty();
public SQLBigDecimalTypeColumn<BlogEntityVO1Proxy> score() {
return getBigDecimalTypeColumn("score");
}
public SQLIntegerTypeColumn<BlogEntityVO1Proxy> status() {
return getIntegerTypeColumn("status");
}
public BlogEntityVO1ProxyFetcher FETCHER =
new BlogEntityVO1ProxyFetcher(this, null, SQLSelectAsExpression.empty);
}
That design is very Java-specific. Querydsl solves the same problem with generated Q types. jOOQ solves it with generated schema-based Java models. All three are using compile-time generation to give the DSL something real to stand on.
What makes easy-query interesting is what it puts on top of those generated helpers.
It does not stop at typed columns. It keeps extending the model into:
- relation navigation
- collection semantics
- DTO assignment
- fetcher-based structured selection
- structured object returns
That is why this part matters. The generated code is not just there so the project compiles. It is the foundation that lets the rest of the DSL stay strongly typed even when the query moves beyond simple field predicates.
4. The object mapping story is much stronger than most people expect
This is one of the most underestimated parts of easy-query. A lot of ORMs can fetch data. Much fewer are really good at returning the shape an application actually wants.
Flat DTO projection is the easy part
Of course easy-query can do this:
List<UserDTO> users = easyEntityQuery.queryable(User.class)
.select(UserDTO.class)
.toList();
That is not the interesting part.
The interesting part is this:
List<SysUserDTO> list = easyEntityQuery.queryable(SysUser.class)
.selectAutoInclude(SysUserDTO.class)
.toList();
selectAutoInclude is not just "map rows into DTOs".
It is much closer to:
- let the DTO structure participate in query planning
- load relations according to that structure
- assemble a structured result without falling back to manual object graph assembly
That is a very different level of abstraction.
include, include2, and selectAutoInclude solve different parts of the same problem
This part of the design works well because it is not one overloaded feature pretending to do everything. Instead, the framework gives you three related tools:
-
includefor entity graph loading -
include2for more complex nested graph scenarios -
selectAutoIncludefor DTO-oriented structured results
For example:
List<SchoolClass> list = easyEntityQuery.queryable(SchoolClass.class)
.include(s -> s.schoolTeachers())
.include(s -> s.schoolStudents(), x -> {
x.include(y -> y.schoolStudentAddress())
.orderBy(y -> y.age().desc())
.limit(3);
})
.where(s -> s.name().eq("Class 1"))
.toList();
Or:
List<SchoolClass> list = easyEntityQuery.queryable(SchoolClass.class)
.include2((c, s) -> {
c.query(s.schoolTeachers().flatElement().schoolClasses())
.where(a -> a.name().like("123"));
c.query(s.schoolStudents().flatElement().schoolClass())
.where(x -> x.schoolStudents().flatElement().name().eq("123"));
c.query(s.schoolStudents())
.where(x -> x.name().ne("123"));
})
.toList();
This is where easy-query stops looking like "a nicer query DSL" and starts looking like a framework that treats object graphs as first-class results.
5. whereObject is a much bigger deal than it sounds
whereObject is one of those features that looks small on paper and huge in real business code.
Most enterprise list pages still end up doing some version of this:
- accept a request object
- translate it into predicates manually
- query data
- assemble the result
With easy-query, the request object can move much closer to the query model itself.
For example:
@Data
public class BlogQueryRequest {
@EasyWhereCondition
private String title;
@EasyWhereCondition(type = EasyWhereCondition.Condition.EQUAL)
private Integer star;
@EasyWhereCondition(
type = EasyWhereCondition.Condition.RANGE_LEFT_CLOSED,
propName = "publishTime"
)
private LocalDateTime publishTimeBegin;
@EasyWhereCondition(
type = EasyWhereCondition.Condition.RANGE_RIGHT_CLOSED,
propName = "publishTime"
)
private LocalDateTime publishTimeEnd;
}
Then:
List<BlogEntity> list = easyEntityQuery.queryable(BlogEntity.class)
.whereObject(query)
.toList();
And propName itself can target implicit join or implicit subquery paths.
This is not just "bean to where conversion". It means the query object itself becomes part of the ORM language.
In real systems, that reduces a lot of low-value code:
- manual predicate translation
- duplicated request-to-query glue
- scattered search condition builders
This is exactly the kind of feature that makes a framework feel more natural in real applications over time.
6. Dynamic queries stay inside the DSL
This is another area where easy-query feels very practical.
The common pattern is to attach a filter policy directly to the current query:
PageResult<Post> pageResult = easyEntityQuery.queryable(Post.class)
.filterConfigure(NotNullOrEmptyValueFilter.DEFAULT_PROPAGATION_SUPPORTS)
.where(tPost -> {
tPost.title().contains(request.getTitle());
})
.orderBy(tPost -> tPost.publishAt().desc())
.toPageResult(pageIndex, pageSize);
The value here is not "fewer if statements". The value is that dynamic conditions are still part of the DSL lifecycle.
That means:
- value filtering is localized
- relation activation is still part of query semantics
- the same rule can propagate into implicit subqueries when needed
Many frameworks support dynamic queries. Fewer keep them from degrading into manual condition assembly. That difference shows up very quickly in search pages, report filters, and admin backends.
7. The framework has a lot of practical depth
A framework can look great in a demo and still become frustrating once real application constraints show up. easy-query has a broader feature surface than most people expect:
whereObjectorderByObject- relation navigation
- structured DTO returns
- logic delete
- dynamic table naming
- sharding
- encryption-aware querying
- code-first support
- tracking-based differential updates
That last one is especially worth mentioning.
In Spring Boot, the idiomatic tracking flow looks like this:
@EasyQueryTrack
public void updateUserPhone(String id, String phone) {
SysUserTrack user = easyEntityQuery.queryable(SysUserTrack.class)
.asTracking()
.whereById(id)
.firstOrNull();
user.setPhone(phone);
easyEntityQuery.updatable(user).executeRows();
}
This gives you a very different update experience from the usual "push all fields back".
Again, this is not just about API beauty. It is about how much repetitive business plumbing the framework is willing to absorb for you.
That is why easy-query feels much closer to an application-facing ORM platform than to a simple query DSL.
8. The performance angle is real, but it is not the main story
In most real business systems, ORM overhead is not the main performance bottleneck.
Usually, the real costs are still:
- SQL shape
- execution plans
- round trips
- data volume
So it does not make sense to sell easy-query as "the framework that wins because object mapping is 10% faster". That would be the wrong story.
What I would say is this: the implementation shows a lot of engineering discipline.
For example, in DefaultFastBean, getter/setter/constructor access is built with MethodHandle + LambdaMetafactory rather than leaning on heavy reflective invocation paths.
That does not magically dominate real-world database cost. But it does tell you something important: the framework is not careless about runtime overhead.
The same goes for:
- metadata caching
- accessor reuse
- batched relation loading
- subquery-to-group-join rewriting
These are not "the reason business performance will be great". They are signs of a framework that tries not to add unnecessary cost on top of the real bottleneck. That is a much more credible claim.
9. Why it already belongs in the top tier
"Most powerful" is a big phrase. But the standard behind it is not that hard to explain.
If what you want most is SQL-first control, jOOQ is still exceptional.
If what you want most is the classic JPA ecosystem and repository-centered persistence, Spring Data JPA still has a very stable place.
If what you want most is type-safe predicate building on top of JPA, Querydsl is still mature.
But if what you want is this combination:
- relation-oriented DSL
- collection semantics
- structured DTO/object graph returns
- object-driven filtering
- practical business utilities
- a framework model that stays coherent as complexity grows
then easy-query is already one of the strongest options in the Java ecosystem.
That is why it does not feel like a tool that merely improves querying. It feels much closer to an attempt to define what a modern Java business ORM should feel like.
And honestly, in several areas, it is already there.
Final Thoughts
What makes easy-query interesting is not that it can query a database. Plenty of frameworks can do that.
What makes it interesting is that it brings together:
- type-safe DSL
- relation semantics
- object mapping
- practical query features
- and a fairly disciplined implementation model
inside one coherent framework.
That coherence is rare. That is the real reason the title no longer feels exaggerated:
easy-query: The Most Powerful ORM for Java
Top comments (0)