danvega / spring-one-books

Repository from Github https://github.comdanvega/spring-one-booksRepository from Github https://github.comdanvega/spring-one-books

SpringOne Books

Agenda

  1. Project Setup
    1. start.spring.io → Spring Boot + Spring for GraphQL
    2. IntelliJ → Run Application
  2. Schema-First Design
    1. create schema.graphqls
    2. Add Book & Author Object Types
    3. Add Query Mappings (Operation Types) (without filter)
    4. Run Application → Inspect Schema Mapping Inspection Report
  3. Core Implementation
    1. Simple Book and Author Records
    2. In-memory data setup (List of books/authors)
    3. @SchemaMapping & @QueryMapping
    4. GraphiQL → test /books and book by id
  4. Query Filtering
    1. Add BookInput Type to schema
    2. Update books query to include filter
    3. Update data fetcher
  5. Batch Loading
    1. Show the n+1 problem
    2. How does @BatchMapping fix this issue
  6. Union Type
    1. Add Search Item Union to Schema
    2. search(text: String!): [SearchItem!]!
    3. Search Data Fetcher in Book Controller
    4. 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)

Schema-First Design

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

Core Implementation

Simple Book and Author Records

public record Book(
        String id,
        String title,
        Author author,
        Integer publishedYear
) {}
public record Author(
        String id,
        String name
) {}

In-memory data setup (List of books/authors)

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

GraphQL Queries

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;
    }
}

About


Languages

Language:Java 100.0%