# 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.