This article presents JMH benchmark results comparing easy-query, jOOQ, and Hibernate, with source code analysis explaining 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:
- Warmup: 10 iterations, 5 seconds each
- Measurement: 15 iterations, 5 seconds each
- Fork: 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.25x vs jOOQ) |
| Select List | 247,088 ops/s | 68,773 ops/s | 141,050 ops/s | π easy-query (3.59x 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.72x vs jOOQ!) |
| Subquery (GROUP BY + HAVING) | 100,725 ops/s | 5,296 ops/s | 15,594 ops/s | π easy-query (19.01x 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.27x vs jOOQ) |
| Batch Insert (1000) | 72.06 ops/s | 43.54 ops/s | 70.66 ops/s | π easy-query (1.66x vs jOOQ) |
| Update by ID | 125,902 ops/s | 100,361 ops/s | 92,470 ops/s | π easy-query (1.25x 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, drawing from techniques used in high-performance frameworks like fastjson2.
1. Lambda Function Mapping Instead of Reflection
This is easy-query's core performance optimization, using the same technique as fastjson2.
The Problem with Traditional Reflection:
Method methodGetId = Bean.class.getMethod("getId");
Bean bean = createInstance();
int value = (Integer) methodGetId.invoke(bean); // Overhead on every call
Reflection invocation involves:
- Entering JVM native method stack
- JNI layer transitions
- C-level checks and operations
easy-query's Solution:
Use LambdaMetafactory to generate function mappings, converting reflection calls to regular method calls:
// 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<Object, ?> 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<Object, ?>) site.getTarget().invokeExact();
}
}
Performance Difference (from fastjson2 benchmarks):
| Method | Time for 10,000 calls |
|---|---|
| Method.invoke (reflection) | 25ms |
| Lambda function mapping | 1ms |
25x improvement!
The key advantages of Lambda function mapping:
- Generated functions can be cached and reused
- Subsequent calls are equivalent to regular Java method calls
- Executed entirely within Java stack, no JNI overhead
2. Property Accessor Caching
easy-query generates Lambda function mappings for each entity property at startup (or first access) and caches them:
// Getter cache
public Property<Object, ?> getBeanGetter(FastBeanProperty prop) {
return getLambdaProperty(prop);
// In practice, with caching:
// return EasyMapUtil.computeIfAbsent(propertyGetterCache, prop.getName(), k -> getLambdaProperty(prop));
}
// Setter cache
public PropertySetterCaller<Object> getBeanSetter(FastBeanProperty prop) {
return getLambdaPropertySetter(prop);
}
// Constructor cache
public Supplier<Object> getBeanConstructorCreator() {
return getLambdaCreate();
}
This means:
- First access: Generate Lambda function (~50ΞΌs)
- Subsequent access: Use cached function (~0.1ΞΌs)
3. Compile-Time Proxy Generation (APT)
easy-query uses APT (Annotation Processing Tool) to generate proxy classes at compile time, not runtime dynamic proxies:
// Compile-time generated proxy class knows all properties
@EntityProxy
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 JDBC API directly:
// easy-query directly creates PreparedStatement
PreparedStatement ps = EasyJdbcExecutorUtil.createPreparedStatement(
connection, sql, parameters, easyJdbcTypeHandler);
ResultSet rs = ps.executeQuery();
5. Lightweight SQL Generation
easy-query's SQL generation is lightweight 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, direct SQL generation.
6. ResultSet Mapping Optimization
Combined with Lambda function mapping, ResultSet-to-object conversion is also zero-overhead:
// Using cached Setter Lambda to set property values
PropertySetterCaller<Object> setter = fastBean.getBeanSetter(prop);
setter.call(entity, resultSet.getObject(columnIndex));
Equivalent to directly calling entity.setName(value), 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's high performance comes from:
-
Core Technology: Using
LambdaMetafactoryto generate function mappings instead of reflection (same as fastjson2) - Compile-Time Optimization: APT-generated proxy classes, avoiding runtime dynamic proxies
- Lean Architecture: Direct JDBC, no extra abstraction layers
- Caching Strategy: Function mappings and metadata all cached for reuse
As the fastjson2 author states:
Lambda uses LambdaMetafactory to 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.
easy-query applies this philosophy throughout the ORM, achieving near-native JDBC performance while providing advanced features like strong typing, Lambda syntax, and implicit joins.
Related Links
- Benchmark Code: https://github.com/wzszsw/sql-benchmark
- easy-query GitHub: https://github.com/dromara/easy-query
- jOOQ: https://www.jooq.org/
Top comments (0)