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
This commit is contained in:
2026-09-17 19:57:41 +00:00
parent 8d0efb0d4b
commit baff043748
29 changed files with 1454 additions and 1 deletions
@@ -0,0 +1,122 @@
# Schema, entities, and why there's no `@ManyToOne`
The schema is deliberately small:
```graphql
type Query {
books: [Book!]!
}
type Book {
id: ID!
title: String!
author: Author!
}
type Author {
id: ID!
name: String!
}
```
Two queryable types, one relationship. `books` returns every `Book`, and each `Book` carries a
non-null `author`. That single non-null arrow (`author: Author!`, and for that matter `[Book!]!`
on the list itself) does two jobs in this module: it's the ordinary shape of a schema-first API,
and it's the trigger for docs/04-the-null-propagation-trap.md later on.
On the JPA side, `Book` does **not** map that relationship as a `@ManyToOne`:
```java
@Entity
@Table(name = "BOOK")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private Long authorId;
// ...
}
```
`authorId` is a plain `Long` column, not a Hibernate association. That's not an oversight — it's
the point of the module. If `Book.author` were a real `@ManyToOne`, Hibernate's own lazy-loading
and fetch-join machinery would decide when the `AUTHOR` row gets loaded, and that decision would
tangle with GraphQL Java's field-resolution order in ways that are hard to observe cleanly. Keeping
`authorId` as an inert column means the *only* thing that decides when and how an `Author` gets
loaded is the GraphQL resolver wired up for the `Book.author` field — which is exactly what
docs/02 and docs/03 compare.
## Three resolver-relevant annotations
`BookController` supplies the query root with an ordinary `@QueryMapping`:
```java
@Controller
public class BookController {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@QueryMapping
public List<Book> books() {
return bookRepository.findAll();
}
}
```
Source: [`resolver/BookController.java`](../src/main/java/com/ankurm/graphqldataloader/resolver/BookController.java)
That's one `SELECT` for the whole list — `select b1_0.id,b1_0.author_id,b1_0.title from book b1_0`,
confirmed in every transcript under `docs/output/` (line 1 of each one). The interesting part is
what happens next, when GraphQL Java has a list of `Book` objects and needs to resolve `author` on
each of them. Two resolver classes answer that question differently, and both are wired into the
same schema and the same entities — only the annotation and the method signature change. They're
kept apart with Spring profiles (`@Profile("naive")` / `@Profile("batched")`) so a single running
instance never has both active at once, and the test suite in docs/02docs/04 boots one profile
per test class to compare them under identical data.
## `application.yml` and `schema.sql`
```yaml
spring:
sql:
init:
mode: always
jpa:
hibernate:
ddl-auto: none
open-in-view: false
graphql:
graphiql:
enabled: true
```
Source: [`src/main/resources/application.yml`](../src/main/resources/application.yml)
`open-in-view: false` is not incidental. With OSIV on, a stray Hibernate session spanning the whole
request can paper over exactly the kind of lazy-loading-triggered-from-the-wrong-place bug this
module is built to make visible; turning it off means any accidental session access outside the
transaction that a resolver runs in fails loudly instead of quietly working by accident.
```sql
create table author (
id bigint auto_increment primary key,
name varchar(200) not null
);
create table book (
id bigint auto_increment primary key,
title varchar(300) not null,
author_id bigint not null
);
```
Source: [`src/main/resources/schema.sql`](../src/main/resources/schema.sql)
No foreign key constraint on `book.author_id`. That's also deliberate — docs/04 seeds a `Book` row
whose `author_id` matches no `Author` row at all, and a foreign key would make that impossible to
set up in the first place.
@@ -0,0 +1,77 @@
# The naive resolver, and exactly how it becomes N+1
`NaiveAuthorResolver` is the resolver nobody would flag in code review:
```java
@Profile("naive")
@Controller
public class NaiveAuthorResolver {
private final AuthorRepository authorRepository;
public NaiveAuthorResolver(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
@SchemaMapping(typeName = "Book", field = "author")
public Author author(Book book) {
return authorRepository.findById(book.getAuthorId()).orElse(null);
}
}
```
Source: [`resolver/NaiveAuthorResolver.java`](../src/main/java/com/ankurm/graphqldataloader/resolver/NaiveAuthorResolver.java)
One method, one repository call, correct output for any individual `Book`. `@SchemaMapping`
targets a specific field of a specific type (`Book.author`), and Spring GraphQL calls that method
once per `Book` instance GraphQL Java needs to resolve the field for — independently, with no
visibility into what the other invocations are doing. If the query asks for 5 books, this method
runs 5 times; if it asks for 20, it runs 20 times. That's not a framework limitation being worked
around badly, it's simply what `@SchemaMapping` is: a per-object field resolver, full stop.
## The evidence, at two scales
Test class: [`NaiveResolverSqlLogTest`](../src/test/java/com/ankurm/graphqldataloader/NaiveResolverSqlLogTest.java),
run with `@ActiveProfiles("naive")`. Every query in these transcripts came from a real HTTP POST to
`/graphql` on a live, randomly-ported embedded Tomcat instance under `@SpringBootTest`, captured by
the JDK dynamic proxy in `StatementLoggingDataSource` — the same mechanism from the sdjpa4-demo
companion project — not from Hibernate's own SQL logging, so what's counted is what actually
reached H2.
**5 books, 5 distinct authors** (`a_fiveBooksFromFiveDistinctAuthorsIssueOneAuthorQueryPerBook`):
```
1. select b1_0.id,b1_0.author_id,b1_0.title from book b1_0
2. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
3. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
4. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
5. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
6. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
total statements: 6
```
Source: [docs/output/naive-a-five-distinct-authors.txt](output/naive-a-five-distinct-authors.txt)
One `books` query, five `author` queries. That's the "N+1" in its cleanest form: 1 query for the
list, N queries for the thing every item in the list needs.
**20 books, but only 5 distinct authors** (`b_twentyBooksFromFiveAuthorsStillIssuesTwentyAuthorQueries`) —
each author now has 4 books:
```
1. select b1_0.id,b1_0.author_id,b1_0.title from book b1_0
2. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
3. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
...
21. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
total statements: 21
```
Full transcript: [docs/output/naive-b-twenty-books-five-authors.txt](output/naive-b-twenty-books-five-authors.txt)
This is the case worth sitting with. There are only 5 *distinct* authors involved — a batched
lookup could satisfy the whole request with a single `WHERE id IN (...)` query. The naive resolver
issues 21 statements anyway, because `findById()` inside the per-`Book` callback has no way to know
that the author it's about to fetch for book #6 is the same author it already fetched for book #2.
Repetition in the data doesn't help this resolver at all — it costs exactly as much as 20 books by
20 different authors would have. That's the concrete cost `@BatchMapping` fixes in docs/03, and the
`.distinct()` call that makes the fix worth having.
@@ -0,0 +1,104 @@
# `@BatchMapping`: the same field, one annotation different
`BatchedAuthorResolver` answers the exact same schema field — `Book.author` — with a method that
takes every pending `Book` at once instead of one at a time:
```java
@Profile("batched")
@Controller
public class BatchedAuthorResolver {
private final AuthorRepository authorRepository;
public BatchedAuthorResolver(AuthorRepository authorRepository) {
this.authorRepository = authorRepository;
}
@BatchMapping(typeName = "Book", field = "author")
public Map<Book, Author> author(List<Book> books) {
List<Long> authorIds = books.stream().map(Book::getAuthorId).distinct().toList();
Map<Long, Author> byId = authorRepository.findAllById(authorIds).stream()
.collect(Collectors.toMap(Author::getId, a -> a));
Map<Book, Author> result = new HashMap<>();
for (Book book : books) {
Author author = byId.get(book.getAuthorId());
if (author != null) {
result.put(book, author);
}
}
return result;
}
}
```
Source: [`resolver/BatchedAuthorResolver.java`](../src/main/java/com/ankurm/graphqldataloader/resolver/BatchedAuthorResolver.java)
Three things changed relative to `NaiveAuthorResolver`, and only one of them is the annotation:
1. `@BatchMapping` instead of `@SchemaMapping`. Spring GraphQL registers this method as a batch
loading function for `Book.author` and wires it into a per-GraphQL-request `DataLoader`
automatically — there's no `BatchLoaderRegistry` bean to declare for this shortcut form; the
annotation alone is enough. Under the hood, GraphQL Java's execution engine collects every
pending `Book.author` field resolution for the current batch of the current request before
calling this method once with the full list, rather than calling a per-object resolver N times.
2. The method signature takes `List<Book> books`, not a single `Book`, and returns
`Map<Book, Author>` — the DataLoader machinery matches each `Book` key back to its `Author`
value and resolves the individual GraphQL fields from that map.
3. `authorIds` is built with `.distinct()` before it ever reaches the repository. This is the line
that turns "one query instead of N" into "one query for the *distinct* authors involved,"
regardless of how many books repeat the same author. Without it, `findAllById()` would still be
a single query, just one with duplicate IDs in its `IN (...)` list — correct, but not as tight
as it could be.
The returned `Map` also deliberately **omits** an entry for any `Book` whose author lookup misses,
rather than putting an explicit `null` value in for it. That choice doesn't change anything for the
well-formed data in this chapter — it's the setup for docs/04, where a `Book` with no matching
`Author` row shows that Spring GraphQL treats a missing map key exactly the same as an explicit
null.
## The evidence, same two scales as docs/02
Test class: [`BatchedResolverSqlLogTest`](../src/test/java/com/ankurm/graphqldataloader/BatchedResolverSqlLogTest.java),
`@ActiveProfiles("batched")`, same seed helpers, same GraphQL query text, same real-HTTP mechanics
as the naive test class — only the active profile (and therefore which resolver bean is wired to
`Book.author`) differs.
**5 books, 5 distinct authors** (`a_fiveBooksFromFiveDistinctAuthorsIssueOneBatchedAuthorQuery`):
```
1. select b1_0.id,b1_0.author_id,b1_0.title from book b1_0
2. select a1_0.id,a1_0.name from author a1_0 where a1_0.id in (?,?,?,?,?)
total statements: 2
```
Source: [docs/output/batched-a-five-distinct-authors.txt](output/batched-a-five-distinct-authors.txt)
Six statements in the naive transcript became two. The books query is identical — same SQL, same
one round trip — and the five separate `author.id=?` lookups collapsed into a single
`author.id in (?,?,?,?,?)`.
**20 books, 5 distinct authors** (`b_twentyBooksFromFiveAuthorsDedupesToOneBatchedQueryForFiveAuthors`):
```
1. select b1_0.id,b1_0.author_id,b1_0.title from book b1_0
2. select a1_0.id,a1_0.name from author a1_0 where a1_0.id in (?,?,?,?,?)
total statements: 2
```
Source: [docs/output/batched-b-twenty-books-five-authors.txt](output/batched-b-twenty-books-five-authors.txt)
Still two statements — the same two, in fact, since the `IN` clause still has exactly 5
placeholders (asserted directly in the test by counting `?` characters in the captured SQL). Where
the naive resolver went from 6 statements to 21 as the book count quadrupled with no new authors,
the batched resolver's query count didn't move at all. The `.distinct()` call is doing real work
here: 20 pending DataLoader keys, 5 unique values, 1 query.
| | 5 books / 5 authors | 20 books / 5 authors |
|---|---|---|
| naive (`@SchemaMapping`) | 6 statements | 21 statements |
| batched (`@BatchMapping`) | 2 statements | 2 statements |
That table is the whole argument for `@BatchMapping` in one place: the naive resolver's cost is a
function of how many books are in the result; the batched resolver's cost is a function of how many
*distinct* authors are in the result, which is very often a much smaller, much more slowly growing
number.
@@ -0,0 +1,73 @@
# A dangling foreign key, a non-null field, and the whole response going null
`schema.sql` deliberately puts no foreign key constraint on `book.author_id` (see docs/01), which
makes it possible to insert a `Book` whose `authorId` matches no row in `AUTHOR` at all — a data
integrity problem that a real foreign key would normally prevent, but one that's entirely possible
in systems that don't enforce referential integrity at the database level, or during a migration,
or from a bad import. Both test classes run the same experiment against it:
```java
Author real = authorRepository.save(new Author("Ursula K. Le Guin"));
bookRepository.save(new Book("The Dispossessed", real.getId()));
bookRepository.save(new Book("Orphan Book With No Author Row", 9_999_999L));
```
Two books, one with a valid author, one with an `authorId` (`9999999`) that doesn't exist. Then the
same query as docs/02 and docs/03:
```graphql
{ books { title author { name } } }
```
## What happens with the naive resolver
`NaiveAuthorResolver.author()` runs `authorRepository.findById(9_999_999L).orElse(null)` for the
orphan book and gets back `null`. The schema says `author: Author!` — non-null. GraphQL Java's
response, captured verbatim from the real HTTP response body
(`c_danglingAuthorIdNullsTheEntireBooksListViaNonNullPropagation`):
```json
{"errors":[{"message":"The field at path '/books[1]/author' was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value. The graphql specification requires that the parent field be set to null, or if that is non nullable that it bubble up null to its parent and so on. The non-nullable type is 'Author' within parent type 'Book'","path":["books",1,"author"],"extensions":{"classification":"NullValueInNonNullableField"}},"data":null}
```
Source: [docs/output/naive-c-dangling-foreign-key-null-propagation.txt](output/naive-c-dangling-foreign-key-null-propagation.txt)
Read that error message once, slowly, because it names its own mechanism: a non-null field that
resolves to null doesn't just fail *that* field — the null has to go somewhere, and the GraphQL
specification's rule is that it propagates upward to the nearest field in the response tree that
*is* allowed to be null. Here, the chain is `author` (non-null) inside `Book` (non-null element of
a non-null list, `[Book!]!`) inside `books` (non-null). There is no nullable field anywhere between
the failure and the root, so the null keeps climbing until it reaches the top: the entire `data`
key becomes `null`, and the one broken book takes the four working ones down with it. `errors` is
populated, `data` is not partial — it's entirely absent.
## What happens with the batched resolver
`BatchedAuthorResolver.author()` builds its result map from `authorRepository.findAllById(...)`,
and the orphan book's `authorId` simply never appears as a key in that map — the resolver's own
doc comment calls this out explicitly: a missing map entry, not an explicit null value stored
against the key. The response, from the same experiment run against the `batched` profile:
```json
{"errors":[{"message":"The field at path '/books[1]/author' was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value. The graphql specification requires that the parent field be set to null, or if that is non nullable that it bubble up null to its parent and so on. The non-nullable type is 'Author' within parent type 'Book'","path":["books",1,"author"],"extensions":{"classification":"NullValueInNonNullableField"}},"data":null}
```
Source: [docs/output/batched-c-dangling-foreign-key-null-propagation.txt](output/batched-c-dangling-foreign-key-null-propagation.txt)
Byte-for-byte the same error, same path, same classification. That's the point of running the
experiment against both profiles rather than just one: Spring GraphQL treats "no entry in the
`Map<Book, Author>` returned from a `@BatchMapping` method" as identical to "no `Author` found from
a per-object `@SchemaMapping` resolver" — the null-propagation behavior lives entirely in GraphQL
Java's execution engine, not in either resolver strategy, and switching from naive to batched buys
better query counts without changing this failure mode at all.
## Why this matters more than it looks like it does
Non-null propagation is a deliberate, spec-mandated safety property — it exists so a client can
trust that a non-null field, if present, is never actually null, which simplifies every consumer of
the API. But it means a single bad row, one dangling foreign key in one book out of however many,
is enough to null out an entire query's `data`, including books whose own data was completely fine.
In a production schema, the practical response to this is usually one of: make the field nullable
if "we don't know the author" is a legitimate state; enforce the foreign key at the database layer
so the dangling reference can't be created in the first place; or have the resolver return a
sentinel "unknown author" object instead of `null`/a missing map entry. This module does none of
those on purpose, specifically so the failure mode is visible and reproducible rather than designed
around.
+100
View File
@@ -0,0 +1,100 @@
# Two Boot 4.1 packaging changes this module ran into directly
Neither of these is about GraphQL or DataLoader batching — they're genuine build-time surprises
from targeting Spring Boot 4.1.1 that are worth recording here because they cost real debugging
time and because searching for the old, Boot-3-era package names will still turn up plenty of
now-stale documentation and Stack Overflow answers.
## `DataSourceAutoConfiguration` moved packages
The very first `mvn clean package` on this module failed with:
```
[ERROR] package org.springframework.boot.autoconfigure.jdbc does not exist
[ERROR] cannot find symbol
symbol: class DataSourceAutoConfiguration
```
`@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)` is used here for the same
reason it's used in the sdjpa4-demo companion project: to stop Boot's own auto-configuration from
building a `DataSource` from `application.yml` properties, so the custom `@Bean DataSource` method
wrapping H2 in `StatementLoggingDataSource` is the only one that ever runs. In Spring Boot 3, that
class lived at `org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration`, inside
the single, large `spring-boot-autoconfigure` artifact.
As of Boot 4.1.1, `spring-boot-autoconfigure` only contains the generic auto-configuration
*infrastructure*`AutoConfigurationImportSelector`, `AutoConfigurations`, and similar — confirmed
directly by listing the jar:
```
$ unzip -l spring-boot-autoconfigure-4.1.1.jar | grep -i jdbc
(no output)
```
The actual per-feature auto-configuration classes now live in their own per-feature artifacts, in
new packages. `DataSourceAutoConfiguration` specifically is in `spring-boot-jdbc-4.1.1.jar`, at
`org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration` — the `jdbc` and
`autoconfigure` segments swapped relative to the old package name. The fix was a one-line import
change:
```diff
-import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
```
The sdjpa4-demo companion project (for the Spring Data JDBC vs. JPA article) already used the
correct new package — this module's `GraphqlDataloaderApplication.java` was written from habit
before that was double-checked here directly.
## Jackson 3 by default
Writing the HTTP-level test assertions for docs/02docs/04 initially used
`com.fasterxml.jackson.databind.{ObjectMapper,JsonNode}`, which failed to compile:
```
[ERROR] package com.fasterxml.jackson.databind does not exist
```
`mvn dependency:tree` on this module shows why:
```
+- org.springframework.boot:spring-boot-starter-jackson:jar:4.1.1:compile
| \- org.springframework.boot:spring-boot-jackson:jar:4.1.1:compile
| \- tools.jackson.core:jackson-databind:jar:3.1.5:compile
| +- com.fasterxml.jackson.core:jackson-annotations:jar:2.21:compile
| \- tools.jackson.core:jackson-core:jar:3.1.5:compile
```
Spring Boot 4.1's default Jackson integration is Jackson 3, whose `jackson-core` and
`jackson-databind` modules moved to the `tools.jackson.core` Maven group and the
`tools.jackson.databind` / `tools.jackson.core` Java packages — a deliberate break from the
`com.fasterxml.jackson.*` namespace Jackson 2 used. `jackson-annotations` is the one piece that
stayed on the old `com.fasterxml.jackson.core` group (at 2.21), since annotations are largely
shared/compatible across the 2.x/3.x line. The fix in this module's test classes was the same shape
as the `DataSourceAutoConfiguration` one — swap the import, not the code:
```diff
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
```
`ObjectMapper.writeValueAsString(...)` and `.readTree(...)` behave identically once the import is
fixed — this module didn't need any further changes to its JSON-handling code. Projects still on
`spring-boot-starter-web`/`spring-boot-starter-json` from a Boot 3.x background, or projects that
pull in `com.fasterxml.jackson.core:jackson-databind` directly rather than through a Boot starter,
should expect to hit this the first time they add any code that touches `ObjectMapper` or
`JsonNode` directly after upgrading to Boot 4.1.
## What was, and wasn't, independently verified here
Every SQL statement count and every GraphQL error message in docs/02docs/04 came from this
module's own test runs against real H2 and a real embedded Tomcat instance — nothing in those three
chapters is asserted from documentation or memory. The two packaging changes above were confirmed
directly by inspecting the actual `.jar` contents downloaded into the local Maven repository for
this build (`unzip -l`), not by trusting a changelog summary. What was *not* independently
re-derived from first principles here is the general architectural claim that Spring Boot 4
modularized auto-configuration into per-feature artifacts — that's stated as context, based on what
this module's own dependency tree and jar contents show, not as an exhaustively researched survey
of every module that moved.
@@ -0,0 +1,6 @@
batched profile / 5 books, 5 distinct authors (no repeats to dedupe)
1. select b1_0.id,b1_0.author_id,b1_0.title from book b1_0
2. select a1_0.id,a1_0.name from author a1_0 where a1_0.id in (?,?,?,?,?)
total statements: 2
@@ -0,0 +1,6 @@
batched profile / 20 books, 5 distinct authors (4 books per author) — one batched IN query
1. select b1_0.id,b1_0.author_id,b1_0.title from book b1_0
2. select a1_0.id,a1_0.name from author a1_0 where a1_0.id in (?,?,?,?,?)
total statements: 2
@@ -0,0 +1,4 @@
batched profile / one Book has authorId=9999999 which matches no Author row
raw HTTP response body from POST /graphql:
{"errors":[{"message":"The field at path '/books[1]/author' was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value. The graphql specification requires that the parent field be set to null, or if that is non nullable that it bubble up null to its parent and so on. The non-nullable type is 'Author' within parent type 'Book'","path":["books",1,"author"],"extensions":{"classification":"NullValueInNonNullableField"}}],"data":null}
@@ -0,0 +1,10 @@
naive profile / 5 books, 5 distinct authors (no repeats to dedupe)
1. select b1_0.id,b1_0.author_id,b1_0.title from book b1_0
2. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
3. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
4. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
5. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
6. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
total statements: 6
@@ -0,0 +1,25 @@
naive profile / 20 books, 5 distinct authors (4 books per author) — repeats do not help
1. select b1_0.id,b1_0.author_id,b1_0.title from book b1_0
2. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
3. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
4. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
5. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
6. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
7. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
8. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
9. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
10. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
11. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
12. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
13. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
14. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
15. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
16. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
17. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
18. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
19. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
20. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
21. select a1_0.id,a1_0.name from author a1_0 where a1_0.id=?
total statements: 21
@@ -0,0 +1,4 @@
naive profile / one Book has authorId=9999999 which matches no Author row
raw HTTP response body from POST /graphql:
{"errors":[{"message":"The field at path '/books[1]/author' was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value. The graphql specification requires that the parent field be set to null, or if that is non nullable that it bubble up null to its parent and so on. The non-nullable type is 'Author' within parent type 'Book'","path":["books",1,"author"],"extensions":{"classification":"NullValueInNonNullableField"}}],"data":null}