# `@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 author(List books) { List authorIds = books.stream().map(Book::getAuthorId).distinct().toList(); Map byId = authorRepository.findAllById(authorIds).stream() .collect(Collectors.toMap(Author::getId, a -> a)); Map 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 books`, not a single `Book`, and returns `Map` — 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.