- naive @SchemaMapping resolver: 6 statements (5 books/5 authors), 21 statements (20 books/5 authors) - batched @BatchMapping resolver: flat 2 statements in both cases, via DataLoader + .distinct() - dangling authorId nulls the entire GraphQL response via non-null propagation, byte-identical under both resolver strategies - 6-test suite over real HTTP against a live embedded Tomcat instance, SQL captured via a JDK dynamic proxy (StatementLoggingDataSource, reused from sdjpa4-demo) - docs/05: two Boot 4.1 packaging changes hit along the way (DataSourceAutoConfiguration's new package, Jackson 3 by default) - root README: add row for graphql-dataloader; fix openapi-versioning's placeholder link now that post 7477 is live
3.9 KiB
graphql-dataloader
Companion code for Spring GraphQL 2.0: Schema-First APIs, DataLoader Batching and Killing N+1 on ankurm.com.
One schema, one Book.author field, two resolver implementations behind Spring profiles — a naive
@SchemaMapping and a @BatchMapping — with a JDK dynamic proxy in front of the JDBC driver
counting every SQL statement each one actually sends to the database, and a deliberately dangling
foreign key to observe GraphQL's non-null propagation up close.
Tested with: Spring Boot 4.1.1 / Spring Framework 7.0.9 / Spring GraphQL 2.0.5 / GraphQL Java 25.0 / java-dataloader 6.0.0 / JDK 25 (Temurin 25.0.4.1+1).
Quickstart
mvn test # runs everything, regenerates docs/output/
mvn spring-boot:run -Dspring-boot.run.profiles=naive # then, in another shell:
curl -s -X POST http://localhost:8080/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ books { title author { name } } }"}'
mvn spring-boot:run -Dspring-boot.run.profiles=batched # same query, batched resolver
open http://localhost:8080/graphiql # interactive GraphiQL, either profile
Where things are
Schema, entities, and why there's no @ManyToOne |
docs/01-schema-and-resolvers.md |
| The naive resolver, and exactly how it becomes N+1 | docs/02-the-n-plus-one-problem.md |
@BatchMapping: the same field, one annotation different |
docs/03-the-batchmapping-fix.md |
| A dangling foreign key and non-null propagation | docs/04-the-null-propagation-trap.md |
| Two Boot 4.1 packaging changes this module ran into | docs/05-known-issues.md |
Captured output
Every SQL statement count and every GraphQL error message quoted in the article is one of these
files, regenerated by mvn test:
| File | What it shows |
|---|---|
| docs/output/naive-a-five-distinct-authors.txt | naive profile, 5 books / 5 authors — 6 statements |
| docs/output/naive-b-twenty-books-five-authors.txt | naive profile, 20 books / 5 authors — 21 statements |
| docs/output/naive-c-dangling-foreign-key-null-propagation.txt | naive profile, orphan authorId — data: null |
| docs/output/batched-a-five-distinct-authors.txt | batched profile, 5 books / 5 authors — 2 statements |
| docs/output/batched-b-twenty-books-five-authors.txt | batched profile, 20 books / 5 authors — 2 statements |
| docs/output/batched-c-dangling-foreign-key-null-propagation.txt | batched profile, orphan authorId — identical data: null |
Test suite
| Class | What it covers |
|---|---|
NaiveResolverSqlLogTest |
@ActiveProfiles("naive") — the three experiments above, against NaiveAuthorResolver |
BatchedResolverSqlLogTest |
@ActiveProfiles("batched") — the same three experiments, against BatchedAuthorResolver |
Both run real HTTP POSTs (java.net.http.HttpClient) against a live, randomly-ported embedded
Tomcat instance under @SpringBootTest, and read SQL statement counts from
StatementLoggingDataSource —
a JDK dynamic proxy over the JDBC driver, reused from the sdjpa4-demo companion project, that
counts what actually reaches H2 rather than trusting Hibernate's own SQL logging.