Benchmark: easy-query vs jOOQ

Published: (December 26, 2025 at 11:45 AM EST)
5 min read
Source: Dev.to

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

ConfigurationValue
OSWindows 10 (Build 26200)
JDKOpenJDK 21.0.9+10‑LTS
DatabaseH2 2.2.224 (In‑Memory)
Connection PoolHikariCP 4.0.3
JMH1.37
easy‑query3.1.66‑preview3
jOOQ3.19.1
Hibernate6.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 Scenarioeasy‑queryjOOQHibernateWinner
Select by ID298 303 ops/s132 786 ops/s264 571 ops/s🏆 easy‑query (2.25× vs jOOQ)
Select List247 088 ops/s68 773 ops/s141 050 ops/s🏆 easy‑query (3.59× vs jOOQ)
COUNT382 545 ops/s197 704 ops/s385 362 ops/sHibernate ≈ easy‑query

Complex Queries

Test Scenarioeasy‑queryjOOQHibernateWinner
JOIN Query138 963 ops/s5 859 ops/s56 437 ops/s🏆 easy‑query (23.72× vs jOOQ)
Subquery (GROUP BY + HAVING)100 725 ops/s5 296 ops/s15 594 ops/s🏆 easy‑query (19.01× vs jOOQ)

Write Operations

Test Scenarioeasy‑queryjOOQHibernateWinner
Single Insert63 866 ops/s50 257 ops/s57 385 ops/s🏆 easy‑query (1.27× vs jOOQ)
Batch Insert (1000)72.06 ops/s43.54 ops/s70.66 ops/s🏆 easy‑query (1.66× vs jOOQ)
Update by ID125 902 ops/s100 361 ops/s92 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)

MethodTime for 10 000 calls
Method.invoke (reflection)25 ms
Lambda function mapping1 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

Optimizationeasy‑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 LambdaMetafactory to 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 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.” – 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.

  • Benchmark Code:
  • easy‑query GitHub:
  • jOOQ:
Back to Blog

Related posts

Read more »