DEV Community

Li
Li

Posted on

Benchmark: easy-query vs jOOQ

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

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

Reflection invocation involves:

  1. Entering JVM native method stack
  2. JNI layer transitions
  3. 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Generated functions can be cached and reused
  2. Subsequent calls are equivalent to regular Java method calls
  3. 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();
}
Enter fullscreen mode Exit fullscreen mode

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() { ... }
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

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:

  1. Core Technology: Using LambdaMetafactory to generate function mappings instead of reflection (same as fastjson2)
  2. Compile-Time Optimization: APT-generated proxy classes, avoiding runtime dynamic proxies
  3. Lean Architecture: Direct JDBC, no extra abstraction layers
  4. 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

Top comments (0)