Files
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

64 lines
3.9 KiB
Markdown

# graphql-dataloader
Companion code for **Spring GraphQL 2.0: Schema-First APIs, DataLoader Batching and Killing N+1**
on [ankurm.com](https://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
```bash
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](docs/01-schema-and-resolvers.md) |
| The naive resolver, and exactly how it becomes N+1 | [docs/02-the-n-plus-one-problem.md](docs/02-the-n-plus-one-problem.md) |
| `@BatchMapping`: the same field, one annotation different | [docs/03-the-batchmapping-fix.md](docs/03-the-batchmapping-fix.md) |
| A dangling foreign key and non-null propagation | [docs/04-the-null-propagation-trap.md](docs/04-the-null-propagation-trap.md) |
| Two Boot 4.1 packaging changes this module ran into | [docs/05-known-issues.md](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](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](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](docs/output/naive-c-dangling-foreign-key-null-propagation.txt) | naive profile, orphan `authorId``data: null` |
| [docs/output/batched-a-five-distinct-authors.txt](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](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](docs/output/batched-c-dangling-foreign-key-null-propagation.txt) | batched profile, orphan `authorId` — identical `data: null` |
## Test suite
| Class | What it covers |
|---|---|
| [`NaiveResolverSqlLogTest`](src/test/java/com/ankurm/graphqldataloader/NaiveResolverSqlLogTest.java) | `@ActiveProfiles("naive")` — the three experiments above, against `NaiveAuthorResolver` |
| [`BatchedResolverSqlLogTest`](src/test/java/com/ankurm/graphqldataloader/BatchedResolverSqlLogTest.java) | `@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`](src/main/java/com/ankurm/graphqldataloader/support/StatementLoggingDataSource.java) —
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.