Files
spring-boot-demo/graphql-dataloader
asmhatre baff043748 Add graphql-dataloader: Spring GraphQL 2.0 DataLoader batching vs naive N+1, and non-null propagation on a dangling FK
- 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
2026-09-17 19:57:41 +00:00
..

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 authorIddata: 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.