- Project Setup
- start.spring.io → Spring Boot + Spring for GraphQL
- IntelliJ → Run Application
- Schema-First Design
- create
schema.graphqls - Add Book & Author Object Types
- Add Query Mappings (Operation Types) (without filter)
- Run Application → Inspect Schema Mapping Inspection Report
- create
- Core Implementation
- Simple
BookandAuthorRecords - In-memory data setup (List of books/authors)
@SchemaMapping&@QueryMapping- GraphiQL → test /books and book by id
- Simple
- Query Filtering
- Add BookInput Type to schema
- Update books query to include filter
- Update data fetcher
- Batch Loading
- Show the n+1 problem
- How does
@BatchMappingfix this issue
- Union Type
- Add Search Item Union to Schema
- search(text: String!): [SearchItem!]!
- Search Data Fetcher in Book Controller
- Show Example Search in GraphiQL
SpringOne Books ├─ sob-author (Author Record) ├─ sob-book (Book Record) ├─ sob-book-controller-start (Book Controller) ├─ sob-book-service (BookService Class) ├─ sob-controller-book-filtering (Book Filtering) ├─ sob-n1-problem (N+1 Problem) ├─ sob-n1-solution (N+1 Solution using Batch Mapping) ├─ sob-readme (README) ├─ sob-schema-final (Completed Schema) ├─ sob-schema-start (Schema-First Design) └─ sob-search-query-mapping (Search Query Mapping)
# Book(id,title,author,publishedYear) -> Author(id,name,books)
# Object Types
type Book {
id: ID!
title: String!
author: Author!
publishedYear: Int
}
type Author {
id: ID!
name: String!
books: [Book!]!
}
# Operation Types
type Query {
books(filter: BookFilter): [Book!]!
book(id: ID!) : Book
authors: [Author!]!
search(text: String!): [SearchItem!]!
}
# Input types
input BookFilter {
title: String
authorName: String
publishedAfter: Int
}
# Union
union SearchItem = Author | Bookpublic record Book(
String id,
String title,
Author author,
Integer publishedYear
) {}public record Author(
String id,
String name
) {}BookService.java
@Service
public class BookService {
private final List<Book> books = new ArrayList<>();
private final List<Author> authors = new ArrayList<>();
@PostConstruct
public void loadData() {
// Core Java Authors
Author joshuaBloch = new Author("1", "Joshua Bloch");
Author herbertSchildt = new Author("2", "Herbert Schildt");
Author raoulgabrielUrma = new Author("3", "Raoul-Gabriel Urma");
Author brianGoetz = new Author("4", "Brian Goetz");
// Spring Framework Authors
Author craigWalls = new Author("5", "Craig Walls");
Author gregTurnquist = new Author("6", "Greg Turnquist");
Author markHeckler = new Author("7", "Mark Heckler");
Author thomasVitale = new Author("8", "Thomas Vitale");
Author joshLong = new Author("9", "Josh Long");
// Spring Ecosystem Authors
Author dineshRajput = new Author("10", "Dinesh Rajput");
Author johnCarnell = new Author("11", "John Carnell");
Author laurentiuSpilca = new Author("12", "Laurentiu Spilca");
Author petriKainulainen = new Author("13", "Petri Kainulainen");
// Architecture & Design Authors
Author rodJohnson = new Author("14", "Rod Johnson");
Author martinFowler = new Author("15", "Martin Fowler");
Author nealFord = new Author("16", "Neal Ford");
// Modern Java Authors
Author kenKousen = new Author("17", "Ken Kousen");
Author dmitryJemerov = new Author("18", "Dmitry Jemerov");
Author venkatSubramaniam = new Author("19", "Venkat Subramaniam");
// Testing & Best Practices Authors
Author petarTahchiev = new Author("20", "Petar Tahchiev");
Author robertMartin = new Author("21", "Robert C. Martin");
Author andrewHunt = new Author("22", "Andrew Hunt");
// Add all authors to the collection
authors.addAll(List.of(
joshuaBloch, herbertSchildt, raoulgabrielUrma, brianGoetz,
craigWalls, gregTurnquist, markHeckler, thomasVitale, joshLong,
dineshRajput, johnCarnell, laurentiuSpilca, petriKainulainen,
rodJohnson, martinFowler, nealFord,
kenKousen, dmitryJemerov, venkatSubramaniam,
petarTahchiev, robertMartin, andrewHunt
));
// Create books
books.addAll(List.of(
// Core Java
new Book("1", "Effective Java", joshuaBloch, 2017),
new Book("2", "Java: The Complete Reference", herbertSchildt, 2021),
new Book("3", "Modern Java in Action", raoulgabrielUrma, 2018),
new Book("4", "Java Concurrency in Practice", brianGoetz, 2006),
// Spring Framework & Boot
new Book("5", "Spring in Action", craigWalls, 2020),
new Book("6", "Spring Boot in Action", craigWalls, 2015),
new Book("7", "Learning Spring Boot 3.0", gregTurnquist, 2022),
new Book("8", "Spring Boot: Up and Running", markHeckler, 2021),
new Book("9", "Cloud Native Spring in Action", thomasVitale, 2021),
new Book("10", "Reactive Spring", joshLong, 2020),
// Spring Ecosystem & Microservices
new Book("11", "Building Microservices with Spring Boot", dineshRajput, 2020),
new Book("12", "Spring Microservices in Action", johnCarnell, 2021),
new Book("13", "Spring Security in Action", laurentiuSpilca, 2020),
new Book("14", "Spring Data JPA", petriKainulainen, 2019),
// Architecture & Design Patterns
new Book("15", "Expert One-on-One J2EE Design and Development", rodJohnson, 2002),
new Book("16", "Patterns of Enterprise Application Architecture", martinFowler, 2002),
new Book("17", "Refactoring", martinFowler, 2019),
new Book("18", "Building Evolutionary Architectures", nealFord, 2017),
// Modern Java Development
new Book("19", "Modern Java Recipes", kenKousen, 2017),
new Book("20", "Kotlin in Action", dmitryJemerov, 2017),
new Book("21", "Java 8 in Action", raoulgabrielUrma, 2014),
new Book("22", "Functional Programming in Java", venkatSubramaniam, 2014),
// Testing & Best Practices
new Book("23", "JUnit in Action", petarTahchiev, 2020),
new Book("24", "Clean Code", robertMartin, 2008),
new Book("25", "The Pragmatic Programmer", andrewHunt, 2019)
));
}
public List<Book> findAll() {
return books;
}
public Book findById(String id) {
return books.stream()
.filter(book -> book.id().equals(id))
.findFirst()
.orElse(null);
}
public List<Author> findAllAuthors() {
return authors;
}
public Author findAuthorById(String id) {
return authors.stream()
.filter(author -> author.id().equals(id))
.findFirst()
.orElse(null);
}
public List<Book> findBooksByAuthorIds(List<String> authorIds) {
return books.stream()
.filter(book -> authorIds.contains(book.author().id()))
.collect(Collectors.toList());
}
}Notes:
- adding devtools enables graphiql
query {
books {
id
title
publishedYear
author {
id
name
}
}
}query {
books(filter: { authorName: "King", publishedAfter: 2000 }) {
title
author { name }
publishedYear
}
}query {
books(filter: {authorName: "Josh Long"}) {
id
title
publishedYear
author {
id
name
}
}
}query {
search(text: "Spring") {
... on Book {
title
author { name }
}
... on Author {
name
}
}
}Final BookController
package dev.danvega.books;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.BatchMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Controller
public class BookController {
private final BookService bookService;
public BookController(BookService bookService) {
this.bookService = bookService;
}
@QueryMapping
public List<Book> books(@Argument BookFilter filter) {
if (filter == null) {
return bookService.findAll();
}
return bookService.findAll().stream()
.filter(book -> matchesFilter(book, filter))
.collect(Collectors.toList());
}
// @QueryMapping
// public List<Book> books() {
// return bookService.findAll();
// }
@QueryMapping
public Book book(@Argument String id) {
return bookService.findById(id);
}
@QueryMapping
public List<Author> authors() {
return bookService.findAllAuthors();
}
@QueryMapping
public List<Object> search(@Argument String text) {
List<Object> results = new ArrayList<>();
// Search books by title
bookService.findAll().stream()
.filter(book -> book.title().toLowerCase().contains(text.toLowerCase()))
.forEach(results::add);
// Search authors by name
bookService.findAllAuthors().stream()
.filter(author -> author.name().toLowerCase().contains(text.toLowerCase()))
.forEach(results::add);
return results;
}
// This creates the N+1 problem!
@SchemaMapping
public List<Book> books(Author author) {
System.out.println("Loading books for author: " + author.name());
return bookService.findAll().stream()
.filter(book -> book.author().id().equals(author.id()))
.collect(Collectors.toList());
}
@BatchMapping
public Map<Author, List<Book>> books(List<Author> authors) {
System.out.println("🚀 BATCH LOADING books for " + authors.size() + " authors in ONE call!");
List<String> authorIds = authors.stream()
.map(Author::id)
.toList();
List<Book> allBooks = bookService.findBooksByAuthorIds(authorIds);
System.out.println("✅ Loaded " + allBooks.size() + " books total");
Map<Author, List<Book>> booksByAuthor = allBooks.stream()
.collect(Collectors.groupingBy(Book::author));
// Ensure every author has an entry, even if empty
for (Author author : authors) {
booksByAuthor.putIfAbsent(author, Collections.emptyList());
}
return booksByAuthor;
}
private boolean matchesFilter(Book book, BookFilter filter) {
if (filter.authorName() != null &&
!book.author().name().toLowerCase().contains(filter.authorName().toLowerCase())) {
return false;
}
if (filter.publishedAfter() != null &&
book.publishedYear() < filter.publishedAfter()) {
return false;
}
return true;
}
}