- 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
64 lines
3.9 KiB
Markdown
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.
|