Benchmark: easy-query vs jOOQ
Source: Dev.to
Overview
This article presents JMH benchmark results comparing easy‑query, jOOQ, and Hibernate.
Source‑code analysis explains why easy‑query achieves superior performance.
Test Environment
| Configuration | Value |
|---|---|
| OS | Windows 10 (Build 26200) |
| JDK | OpenJDK 21.0.9+10‑LTS |
| Database | H2 2.2.224 (In‑Memory) |
| Connection Pool | HikariCP 4.0.3 |
| JMH | 1.37 |
| easy‑query | 3.1.66‑preview3 |
| jOOQ | 3.19.1 |
| Hibernate | 6.4.1.Final |
Test Parameters
- Warm‑up: 10 iterations, 5 s each
- Measurement: 15 iterations, 5 s each
- Forks: 3 JVM processes
- Threads: Single‑threaded
Benchmark Results
Query Operations
| Test Scenario | easy‑query | jOOQ | Hibernate | Winner |
|---|---|---|---|---|
| Select by ID | 298 303 ops/s | 132 786 ops/s | 264 571 ops/s | 🏆 easy‑query (2.25× vs jOOQ) |
| Select List | 247 088 ops/s | 68 773 ops/s | 141 050 ops/s | 🏆 easy‑query (3.59× vs jOOQ) |
| COUNT | 382 545 ops/s | 197 704 ops/s | 385 362 ops/s | Hibernate ≈ easy‑query |
Complex Queries
| Test Scenario | easy‑query | jOOQ | Hibernate | Winner |
|---|---|---|---|---|
| JOIN Query | 138 963 ops/s | 5 859 ops/s | 56 437 ops/s | 🏆 easy‑query (23.72× vs jOOQ) |
| Subquery (GROUP BY + HAVING) | 100 725 ops/s | 5 296 ops/s | 15 594 ops/s | 🏆 easy‑query (19.01× vs jOOQ) |
Write Operations
| Test Scenario | easy‑query | jOOQ | Hibernate | Winner |
|---|---|---|---|---|
| Single Insert | 63 866 ops/s | 50 257 ops/s | 57 385 ops/s | 🏆 easy‑query (1.27× vs jOOQ) |
| Batch Insert (1000) | 72.06 ops/s | 43.54 ops/s | 70.66 ops/s | 🏆 easy‑query (1.66× vs jOOQ) |
| Update by ID | 125 902 ops/s | 100 361 ops/s | 92 470 ops/s | 🏆 easy‑query (1.25× vs jOOQ) |
Visualization
SELECT BY ID (ops/s – higher is better)
EasyQuery █████████████████████████████████████████████████ 298,303
Hibernate ████████████████████████████████████████████ 264,571
jOOQ ████████████ 132,786
JOIN QUERY (ops/s – higher is better)
EasyQuery █████████████████████████████████████████████████ 138,963
Hibernate ████████████████ 56,437
jOOQ █ 5,859
Why Is easy‑query So Fast?
easy‑query implements deep optimizations at multiple levels, borrowing techniques from high‑performance frameworks such as fastjson2.
1. Lambda Function Mapping Instead of Reflection
The core performance gain comes from replacing reflective method calls with LambdaMetafactory‑generated functions.
Traditional Reflection (slow)
Method methodGetId = Bean.class.getMethod("getId");
Bean bean = createInstance();
int value = (Integer) methodGetId.invoke(bean); // overhead on every call
Reflection incurs:
- JVM native‑method stack entry
- JNI layer transitions
- C‑level checks
easy‑query’s Solution – generate a lambda that directly calls the getter:
// sql-core/src/main/java/com/easy/query/core/common/bean/DefaultFastBean.java
/**
* Fast implementation of bean get/set methods.
* Lambda getter calls have virtually zero overhead.
* Setters are also zero‑overhead except for first‑time lambda creation and caching.
*/
public class DefaultFastBean implements FastBean {
private Property getLambdaProperty(FastBeanProperty prop) {
Class propertyType = prop.getPropertyType();
Method readMethod = prop.getReadMethod();
String getFunName = readMethod.getName();
final MethodHandles.Lookup caller = MethodHandles.lookup();
// Generate function mapping with LambdaMetafactory
final CallSite site = LambdaMetafactory.altMetafactory(
caller,
"apply",
MethodType.methodType(Property.class),
methodType.erase().generic(),
caller.findVirtual(beanClass, getFunName, MethodType.methodType(propertyType)),
methodType,
FLAG_SERIALIZABLE);
return (Property) site.getTarget().invokeExact();
}
}
Performance Difference (fastjson2 benchmark)
| Method | Time for 10 000 calls |
|---|---|
Method.invoke (reflection) | 25 ms |
| Lambda function mapping | 1 ms |
≈ 25× faster!
Key advantages:
- Generated functions can be cached and reused.
- Subsequent calls are equivalent to ordinary Java method calls.
- Execution stays entirely on the Java stack – no JNI overhead.
2. Property‑Accessor Caching
At startup (or on first use) easy‑query creates a lambda for each entity property and stores it in a cache:
// Getter cache
public Property getBeanGetter(FastBeanProperty prop) {
// In practice, with caching:
// return EasyMapUtil.computeIfAbsent(propertyGetterCache, prop.getName(),
// k -> getLambdaProperty(prop));
return getLambdaProperty(prop);
}
// Setter cache
public PropertySetterCaller getBeanSetter(FastBeanProperty prop) {
// return EasyMapUtil.computeIfAbsent(propertySetterCache, prop.getName(),
// k -> getLambdaPropertySetter(prop));
return getLambdaPropertySetter(prop);
}
// Constructor cache
public Supplier getBeanConstructorCreator() {
// return EasyMapUtil.computeIfAbsent(constructorCache, beanClass,
// k -> getLambdaCreate());
return getLambdaCreate();
}
- First access – generate the lambda (≈ 50 µs).
- Subsequent accesses – use the cached lambda (≈ 0.1 µs).
3. Compile‑Time Proxy Generation (APT)
Instead of creating dynamic proxies at runtime, easy‑query uses the Annotation Processing Tool (APT) to generate proxy classes during compilation:
// Compile‑time generated proxy class knows all properties
@EntityProxy
public class User {
// fields, getters, setters, etc. are generated ahead of time
}
- No runtime byte‑code generation → lower start‑up cost.
- All property metadata is available as plain Java code → better JIT optimisation.
Take‑aways
- Lambda‑based accessor generation eliminates the heavy reflection cost.
- Caching turns the first‑time overhead into a one‑off cost.
- Compile‑time proxy generation avoids runtime proxy creation entirely.
Together, these techniques let easy‑query consistently outperform jOOQ and often beat Hibernate, especially on complex queries and batch write operations.
1. Entity Definition & Proxy (Generated)
// Entity class
public class User {
private String id;
private String name;
private Integer age;
}
// Generated proxy class UserProxy
public class UserProxy {
public StringProperty id() { /* ... */ }
public StringProperty name() { /* ... */ }
public IntegerProperty age() { /* ... */ }
}
4. Direct JDBC Execution
easy-query uses the JDBC API directly:
// easy-query directly creates PreparedStatement
PreparedStatement ps = EasyJdbcExecutorUtil.createPreparedStatement(
connection, sql, parameters, easyJdbcTypeHandler);
ResultSet rs = ps.executeQuery();
5. Lightweight SQL Generation
SQL is built by simple string concatenation:
// Clean SQL building
easyEntityQuery.queryable(User.class)
.where(u -> u.age().ge(25))
.orderBy(u -> u.username().desc())
.limit(10)
.toList();
Generated SQL
SELECT id, username, email, age, phone, address
FROM t_user
WHERE age >= ?
ORDER BY username DESC
LIMIT ?
No complex abstraction layers—just direct SQL generation.
6. ResultSet Mapping Optimization
Combined with Lambda‑function mapping, the ResultSet‑to‑object conversion incurs zero overhead:
// Using cached Setter Lambda to set property values
PropertySetterCaller setter = fastBean.getBeanSetter(prop);
setter.call(entity, resultSet.getObject(columnIndex));
This is equivalent to calling entity.setName(value) directly, with no reflection overhead.
easy-query Optimization Summary
| Optimization | easy‑query |
|---|---|
| Lambda function mapping instead of reflection | ✅ |
| Compile‑time proxy classes (APT) | ✅ |
| Direct JDBC execution | ✅ |
| Property accessor caching | ✅ |
| Lightweight SQL generation | ✅ |
Conclusion
easy-query achieves high performance through:
- Core Technology – Uses
LambdaMetafactoryto generate function mappings (same technique as fastjson2). - Compile‑Time Optimization – APT‑generated proxy classes eliminate runtime dynamic proxies.
- Lean Architecture – Direct JDBC usage without extra abstraction layers.
- Caching Strategy – Function mappings and metadata are cached for reuse.
“Lambda uses
LambdaMetafactoryto generate function mappings instead of reflection. Generated functions can be cached for reuse, and subsequent calls are entirely within Java stack calls, virtually identical to native method calls.” – Fastjson2 author
easy-query applies this philosophy throughout the ORM, delivering near‑native JDBC performance while still offering strong typing, Lambda syntax, and implicit joins.
Related Links
- Benchmark Code:
- easy‑query GitHub:
- jOOQ: