# easy-query moves the join out of the SQL and into the entity graph

> easy-query is a Java and Kotlin ORM for JDBC queries whose central idea is that a OneToMany or ManyToMany relationship can be filtered, aggregated and grouped in the where clause, with the subquery and join machinery generated underneath.

**dromara/easy-query** — java/kotlin high performance lightweight solution for jdbc query,support oltp and olap query,一款java下面支持强类型、轻量级、高性能的ORM,致力于解决jdbc查询,拥有对象模型筛选、隐式子查询、隐式join

- Repository: https://github.com/dromara/easy-query
- Website: http://www.easy-query.com
- Stars: 778 · Forks: 79
- Language: Java
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/dromara-easy-query

## Five features that all start with the word implicit

The project describes itself as a Java and Kotlin solution for JDBC queries, and as an ORM under Java that supports strong typing and is aimed at solving JDBC query problems, with object model filtering, implicit subqueries and implicit joins. The homepage is easy-query.com, and the repository is hosted under the dromara organisation.

The feature list is five items and every one of them uses the same idea. Implicit Join automatically implements join queries for OneToOne and ManyToOne relationships, supporting filtering, sorting and result fetching. Implicit Subquery automatically handles subqueries for OneToMany and ManyToMany relationships, again with filtering, sorting and aggregate function results. Implicit Grouping optimises and merges multiple subqueries into grouped queries for those same one-to-many and many-to-many relationships. Implicit Partition Grouping enables first and nth element operations on those collections. And Implicit CASE WHEN Expression provides a property.aggregate.filter() syntax.

Read together, they describe a query builder that does not stop at the entity. A property path such as company.users() is not just a navigation, it is a set of rows you can filter, aggregate or pick the first element from, and the framework works out whether that becomes a join, a subquery or a group by depending on the shape of the expression.

That is a different bet from a traditional ORM, which would give you a lazy collection and let the developer decide how to query it. The bet here is that the awkward part of a JDBC query is the repeated join and subquery boilerplate, and that a strongly typed expression tree can generate it correctly.

The repository is a multi-module Maven project. The module names describe the architecture: sql-core, sql-processor, sql-platform, sql-db-support, sql-extension, sql-ksp-processor and sql-test, with a pom.xml at the root and a pom.xml.versionsBackup file, and the published artefact on Maven Central is com.easy-query/easy-query-all.

## A ManyToOne relationship filters and sorts like a local property

The implicit join example is the shortest way to see the model. The query starts from SysUser and reaches into the company relationship inside the where clause and inside the order by:

```java
List<SysUser> userInXXCompany = entityQuery.queryable(SysUser.class)
        .where(user -> {
            user.company().name().like("xx Company");
        })
        .orderBy(user -> {
            user.company().registerMoney().desc();
            user.birthday().asc();
        }).toList();
```

The API is uniform. user.company() returns a proxy that behaves like a property accessor, and .name() on it produces an expression over the joined company table. The framework notices that the property path crosses a relationship and adds the join, and it does the same for the ordering expression.

Two things follow from the example. First, the join is derived from the object model, so the query cannot reference a relationship the entity does not declare, which is the strong typing the project description claims. Second, the expression is still a set of clauses in a lambda block, ending with toList(), so this is a query builder with a typed surface rather than a repository interface with method names.

The practical limit of implicit join is cardinality. The feature list ties it to OneToOne and ManyToOne, which are the shapes where adding a join does not multiply the rows of the left table. A OneToMany is a different story, and the project has separate machinery for it, which is the subject of the next sections.

## OneToMany collections are queried through a subquery

For a OneToMany or ManyToMany, joining would multiply rows, so the framework does the opposite and uses a subquery. The example filters a company by its users and also compares an aggregate over the same users:

```java
List<Company> companies = entityQuery.queryable(Company.class)
        .where(company -> {
          company.users().any(u -> u.name().like("Xiao Ming"));
          company.users().where(u -> u.name().like("Xiao Ming"))
                  .max(u -> u.birthday()).gt(LocalDateTime.of(2000,1,1,0,0,0));
        }).toList();
```

Two different shapes appear here. The any form asks whether at least one related row matches, which is the classic EXISTS pattern. The where form with a max and a comparison against a LocalDateTime asks for the maximum of a column over the matching related rows, which is the classic aggregate-in-subquery pattern, and the comparison happens outside the subquery so it can still select the company.

Neither shape is written as SQL by the developer, and neither requires the developer to think about aliasing, correlation or how the subquery is joined back to the parent table. That is the value proposition in one snippet: a filter that is awkward to express in a query builder becomes a two-line lambda.

The trade is that the generated SQL is invisible. When a query returns fewer rows than expected, the likely cause is a subtlety in how the framework correlated the subquery, and the only way to find it is to read the generated statement. The README does not document how to log or print that SQL, so treat that as a question to answer before you rely on it.

## Grouping and partition grouping turn collections into aggregates

Two features extend the subquery idea.

Implicit Grouping optimises and merges multiple subqueries into grouped queries for OneToMany and ManyToMany relationships, supporting filtering, sorting and aggregate functions. The example makes the intent explicit in a comment, since without the call the two subqueries would stay separate:

```java
List<Company> companies = entityQuery.queryable(Company.class)
        // Two subqueries in where will be merged
        .subQueryToGroupJoin(company -> company.users())
        .where(company -> {
          company.users().any(u -> u.name().like("Xiao Ming"));
          company.users().where(u -> u.name().like("Xiao Ming"))
                  .max(u -> u.birthday()).gt(LocalDateTime.now());
        }).toList();
```

The call takes the relationship to group on and rewrites the subqueries into a single grouped query, which is the difference between running two subqueries over the same table and joining once and grouping. The project lists supporting filtering, sorting and aggregate functions as part of this feature, so a grouped query can also carry an order by.

Implicit Partition Grouping goes further and enables first and nth element operations on the same relationship shapes. The example sorts the related users and then takes the first or a specific element:

```java
List<Company> companies = entityQuery.queryable(Company.class)
        .where(company -> {
          company.users().orderBy(u->u.birthday().desc()).first().name().eq("Xiao Ming");
          company.users().orderBy(u->u.birthday().desc()).element(0)
                  .birthday().lt(LocalDateTime.now());
        }).toList();
```

The first form asks whether the most recent user matches a name. The second asks about the user at a given position after sorting. Both are the kind of question that turns into a correlated subquery with a window function or an ORDER BY LIMIT in hand written SQL.

The performance question is the one to keep in mind here. Sorting a collection for every parent row is expensive, and the README does not publish any measurement of what these rewrites cost, so an application that leans on partition grouping over a large collection needs its own measurement rather than an assumption.

## Select.DRAFT produces conditional aggregates without a CASE WHEN

The fifth feature is the most SQL-shaped. The property.aggregate.filter() syntax, shown through the Select.DRAFT builder, produces projections where each output column is an aggregate computed only over the rows that match a condition, which is the CASE WHEN WHEN ELSE pattern in its typed form.

The example builds a three column result where each column is a different aggregate over the same filtered set:

```java
List<Draft3<Long, Long, BigDecimal>> result = entityQuery.queryable(SysUser.class)
        .where(user -> {
          user.birthday().lt(LocalDateTime.now());
        })
        .select(user -> Select.DRAFT.of(
                user.id().count().filter(() -> {
                  user.address().eq("Hangzhou");
                }),
                user.id().count().filter(() -> {
                  user.address().eq("Beijing");
                }),
                user.age().avg().filter(() -> {
                  user.address().eq("Beijing");
                })
        )).toList();
```

The shape of the result is named in the type, Draft3 with Long, Long and BigDecimal, so the number of columns and their types are part of the compile time contract. The alternative is a list of Object or a hand written row mapper, and the compiler now checks the projection.

The group by variant of the same feature is what the framework's own single table example uses. The README walks through a complete query over BlogEntity with filtering, aggregation, aggregate filtering, projection and sorting, and it explains one detail that matters for reading generated SQL: the select statement wraps previous expressions as an inline view, the t1 table, and if no subsequent chained operations exist, it expands directly.

That is a precise description of a behaviour that would otherwise surprise you, since whether your filter ends up in an inline view or is inlined depends on what you chain afterwards. A framework that documents this is easier to reason about than one that does not.

## The module split shows where the work happens

The repository layout is a multi-module Maven build, and the module names are a compact description of the architecture. sql-core holds the core model. sql-processor is the query processing layer. sql-platform holds the platform adaptations, which is where the database dialect differences live. sql-db-support is the database support layer. sql-extension holds the extensions, which is where the non-core query features such as the grouping and partition grouping behaviour tend to live. sql-ksp-processor is the Kotlin Symbol Processing implementation, which is how a Kotlin project gets its entity metadata generated at compile time. sql-test is the test module. The published aggregate artefact on Maven Central is com.easy-query/easy-query-all, so most users depend on that rather than on individual modules.

The KSP module is worth a note. Kotlin support is not a separate dialect bolted on, it is a compile time processor in the same style as the annotation processor used for Java, which is how the entity graph gets known to the framework before the query is built.

The framework badges at the top of the README show the environment it targets: JDK 8, JDK 11 and JDK 17, Spring Boot 2.x and Spring Boot 3.x, and Solon 2.x. The topics list adds sharding and mysql to the list alongside java, jdbc, join, kotlin and orm, and the project description says it supports both OLTP and OLAP queries.

Documentation is split across two sites, a GitHub Pages documentation site and an official documentation site at easy-query.com, with the README linking both, plus a Chinese README in the repository and a QQ group for questions. A separate repository publishes an ai skill for the project, which is a new kind of integration surface worth noting if you expect your assistant tooling to know this library.

## Apache-2.0, published on Maven Central, moving on 3.2.x

The project is licensed under Apache-2.0, with the LICENSE file at the repository root and a badge in the README pointing at the Apache licence text. It is published to Maven Central, and the README links the central.sonatype.com search for the project, which is where you resolve the version to depend on.

The README does not include a dependency snippet, so the coordinates are the badge's group and artefact, com.easy-query and easy-query-all, and the version to pin is one of the published ones. Because this is a library consumed through a build tool, the realistic adoption questions are which version to pin and which modules to pull in, not how to install it.

On versions, the recent releases are 3.2.13 on 2026-06-18, 3.2.14 on 2026-08-05 and 3.2.17 on 2026-10-13, and the repository is not archived with the last push on 2026-10-17. So the project is actively changing and the release numbering is on a 3.2.x line, which in Java convention is a stream of fixes and additions inside a stable API.

The upgrade cost is therefore a matter of API drift within a minor line, plus the question of whether a new feature changes the SQL it generates for existing queries. That second point is the one that deserves attention from a team already using the implicit features, because a change in how a collection expression is rewritten changes results and plans without changing your code.

The README also links a Chinese README in the same repository, a QQ group for support, and a documentation site, so most user questions are answered in Chinese first. That is a practical note for an international team evaluating support and documentation coverage.

## Conclusion

easy-query suits a team whose pain is writing the same join and subquery logic by hand in a query builder, especially where one-to-many and many-to-many collections need filtering and aggregation, since those are the cases the implicit subquery, grouping and partition grouping features target. It is the wrong tool for a project that wants full control over every generated statement, because the whole point is that the framework decides the join and the grouping for you. Before adopting, check the module list in the repository against what you need, since the core, the processor, the platform adapters and the KSP processor are separate modules, and confirm that your Spring Boot major version is covered by the 2.x and 3.x badges before you upgrade, since 3.2.17 was published on 2026-10-13 and the project moves quickly.

## FAQ

### What does implicit join mean in easy-query?

It means the framework automatically implements join queries for OneToOne and ManyToOne relationships, so a property path such as user.company().name() adds the join for you, with filtering, sorting and result fetching supported on those paths.

### How does easy-query handle OneToMany and ManyToMany relationships?

With implicit subqueries. The framework automatically handles subqueries for those relationships with filtering, sorting and aggregate function results, and it can merge multiple subqueries into a single grouped query through the implicit grouping and implicit partition grouping features.

### How do I add easy-query to a Java or Kotlin project?

The README does not include a dependency snippet, but the published aggregate artefact on Maven Central is com.easy-query/easy-query-all, and the README links the Maven Central listing for the project. Kotlin users additionally rely on the sql-ksp-processor module, which is the Kotlin Symbol Processing implementation.

### What is the Select.DRAFT builder for?

It produces typed projections where each column is an aggregate computed over rows matching a condition, using the property.aggregate.filter() syntax, which is the typed equivalent of a CASE WHEN expression. The result type names the number of columns and their types, for example Draft3<Long, Long, BigDecimal>.

### Which Java and Spring Boot versions does easy-query support?

The README badges show JDK 8, JDK 11 and JDK 17, Spring Boot 2.x and Spring Boot 3.x, and Solon 2.x. The published versions are on the 3.2.x line, with 3.2.17 released on 2026-10-13.

## Sources

- [dromara/easy-query on GitHub](https://github.com/dromara/easy-query)
- [License: Apache-2.0](https://github.com/dromara/easy-query/blob/main/LICENSE)
- [Project website](http://www.easy-query.com)
- [README](https://github.com/dromara/easy-query/blob/main/README.md)
- [Releases](https://github.com/dromara/easy-query/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/dromara-easy-query
