I built a tool called analyzeHibernateQueries (an extension of ExoBench MCP Server) that takes a JPA mapping and an access path, compiles them, runs them against an ephemeral database on a real Hibernate version in a sandbox, and returns the SQL that was actually prepared plus the statement count. No build of the target application, no local runtime or JVM agent, no instrumentation annotations. It runs Hibernate 5.6 through 7 and EclipseLink 2.7 through 5.0.
The reason I’m posting it here rather than somewhere louder: a lot of threads on this forum are “here’s my mapping, why N selects, will X fix it,” and answering those means reasoning about fetch strategy without a cheap way to verify. This makes verification cheap, and it makes cross-version verification possible, which matters for the “did this change in 6?” questions.
A worked example, from Shopizer OSS 3.2.7 on Hibernate 5.6.15. Its product service resolves a product by SKU, and that one call always emits two statements: a native query turning the SKU into an id, then a load of the product by that id.
I ran the order-list page: 5 orders, 2 lines each, so 10 lines, across 5 distinct SKUs, one of which appears on 4 of the 10 lines. So that’s 10 SKU lookups and 10 by-id loads, all inside one session. The 10 by-id loads emitted 5 statements, one per distinct product, with the repeats served from the first-level cache. The 10 SKU lookups emitted 10 statements caching nothing! Same session, same entities, same number of calls on both halves. The only variable is which key the lookup went through.
The mechanism is not news: the first-level cache is keyed by entity id, so a lookup by business key can’t hit it. What I hadn’t been able to do before was watch both behaviors in one transcript and put a number on the difference, which is what turns “your session cache won’t help there” from an assertion into something a reader can check.
Since ExoBench runs multiple versions, it’s also useful for “did this change?” questions. One small thing I picked up that way: the in-memory pagination warning for setMaxResults with a collection join fetch is HHH000104 on 5.6 and renumbered HHH90003004 on 6.6, which quietly breaks log greps written against the old code.
Where it’s wrong: it detects N+1 by finding repeated statement shapes that differ from the driving query, so it goes silent on recursive walks where the driving query has the same WHERE shape as the follow-ups. I hit this on a hierarchy traversal that cost 10 statements and produced no findings. An empty findings array is evidence, not proof; the workaround is to scale the input and read the raw count.
Disclosure: it’s mine and it’s eventually going to be commercial with a free tier. It’s an MCP server, driven from a coding agent rather than a UI.
What I’d like from this forum is for someone to try and break it. If you have a mapping with a statement count that’s hard to guess, I’d like to see whether ExoBench agrees, and I’ll post the transcripts either way.
Here’s the original post: JOIN FETCH May Not Save You — Seven things JPA developers believe about N+1, checked against the SQL that Hibernate and EclipseLink actually prepared - Blog