This page describes what happens when the application calls authors.findByName("Sylvie Germain"): from the CDI proxy down to the pool, through JDQL → SQL translation and ResultSet → entity materialization. It is also the right place to understand why Mansart favors static codegen.
Overview — repository method call
The whole hot path is reflection-free: QueryPlan instances are static constants prepared at <clinit>, parameter/attribute binding goes through MethodHandle cached once.
Codegen — APT (compile time)
mansart-data-processor is a javax.annotation.processing.Processor that scans compiled CUs:
-
For each class annotated
@Entity(Mansart or JPA) → generatesBook.java(rich metamodel) andBook.java(JPA standard metamodel). -
For each interface annotated
@Repository→ generatesBookRepositoryImpl.java(final public implementation class) + an entry inMETA-INF/mansart-repositories.list.
Source code generated under target/generated-sources/annotations/, marked @Generated("io.vidocq.mansart.data.processor"). The processor has no runtime dependency — it lives only in the consuming module’s APT classpath.
Why APT and not runtime codegen?
-
AOT-friendly — GraalVM native-image, Leyden CDS: no proxy to register, everything is in the bytecode at build time.
-
Diagnosable — generated code is readable, debuggable, visible in stack traces.
-
Zero warmup — no dynamic classloading on first call.
-
JPMS-strict-friendly — generated classes are in the same package as sources, no need for
opens.
Codegen — Class-File API (build time)
mansart-pool generates PooledConnectionProxy at build via JEP 484 Class-File API. The proxy implements java.sql.Connection, delegates every method to the physical connection, and intercepts close() to return the connection to the pool instead of closing it.
Why Class-File API rather than java.lang.reflect.Proxy:
-
Connectionhas 50+ methods — a reflective proxy costs aninvokeper call. -
The Class-File API produces standard bytecode, JIT-friendly from the first call.
-
No ASM / Byte Buddy dependency — matches the ecosystem’s "zero external dependency" rule.
Pool acquire / release
-
Semaphore permits— hard pool bound, fair=false (perf). -
ConcurrentLinkedDeque idle— lock-free stack, push/pop at the head (hot LIFO). -
ConcurrentHashMap.newKeySet inUse— set of borrowed connections, for metrics and leak detection. -
Housekeeper virtual thread — periodic wake-up, evicts connections that are too old or idle, pre-warms up to
minSize.
Details and numbers: see lien:https://codeberg.org/Vidocq/mansart/src/branch/main/mansart-pool/BENCH.md[mansart-pool BENCH.md].
Threading — virtual threads
-
All blocking JDBC executions (
PreparedStatement.execute*,ResultSet.next) run on virtual threads. No platform pool. -
The JDBC connection is never held across a
synchronizedblock that wraps a blocking operation (anti-pattern: thread pinning). -
The transactional connection is carried by
ScopedValue<Connection> CURRENT— automatic propagation intoStructuredTaskScope.fork(…)with no application code. -
mansart-poolvalidates in CI with-Djdk.tracePinnedThreads=fullthat no pinning appears under 1000 parallel borrows.
ResultSet → entity mapping
No reflection. The _Book metamodel captures, in <clinit>, one MethodHandle setter per attribute, obtained via MethodHandles.privateLookupIn(Book.class, lookup). For each row:
Book row = constructor.invoke(); // MethodHandle constructor
_Book.title.setter.invokeExact(row, rs.getString(1));
_Book.author.setter.invokeExact(row, refLoader.get(rs.getLong(2)));
// ...
invokeExact on a MethodHandle is inlined by C2 — perf equivalent to direct field access after warmup.
Transactions — propagation and XA
-
Single-resource (the dominant case) — the
@Transactionalinterceptor opens/closes the transaction. The JDBC connection is enlisted as a degenerateXAResource(onePhase=true). -
Multi-resource (M4) — true 2PC:
preparethencommiton each enlistedXAResource. Recovery log (M5) recordsPREPAREDtransactions that did not complete, for recovery on reboot. -
Suspend / resume —
TransactionManager.suspend()removes the context from the currentScopedValueand returns it;resume(t)reinstates it. Typical batch case: a worker job suspending the caller’s TX to issue a clean INSERT/COMMIT.
See lien:https://codeberg.org/Vidocq/mansart/src/branch/main/mansart-transactions/PLAN.md[mansart-transactions PLAN.md] for the M1-M8 roadmap.