Class 3: Queries Deep Dive
Duration: 30 minutes Difficulty: Intermediate Prerequisites: Completed Classes 1-2
What You'll Learn
By the end of this class, you will:
- Create relationships between types
- Implement field resolvers for nested data
- Understand how GraphQL resolves nested queries
- Use
@SchemaMappingfor type-specific fields - Optimize data fetching with proper resolver design
The Power of Nested Queries
One of GraphQL's superpowers is fetching related data in a single request. Instead of making multiple REST calls, you can write:
query {
movie(id: "1") {
title
director {
name
nationality
}
actors {
name
birthYear
}
}
}
One request. All the data. No over-fetching. Let's build this!
Step 1: Update the Schema with Relationships
Let's connect Movies, Directors, and Actors:
📁 src/main/resources/graphql/schema.graphqls
type Query {
"Get a single movie by its unique ID"
movie(id: ID!): Movie
"Get all movies, optionally filtered by genre"
movies(genre: Genre): [Movie!]!
"Search movies by title"
searchMovies(title: String!): [Movie!]!
"Get a single director by ID"
director(id: ID!): Director
"Get all directors"
directors: [Director!]!
"Get a single actor by ID"
actor(id: ID!): Actor
"Get all actors"
actors: [Actor!]!
}
type Movie {
id: ID!
title: String!
releaseYear: Int!
genre: Genre!
rating: Float
runtime: Int
plot: String
inTheaters: Boolean!
"The director of this movie"
director: Director!
"Actors who appear in this movie"
actors: [Actor!]!
}
type Director {
id: ID!
name: String!
birthYear: Int
nationality: String
"Movies directed by this director"
movies: [Movie!]!
}
type Actor {
id: ID!
name: String!
birthYear: Int
nationality: String
"Movies this actor appears in"
movies: [Movie!]!
}
enum Genre {
ACTION
ANIMATION
COMEDY
CRIME
DOCUMENTARY
DRAMA
FANTASY
HORROR
MUSICAL
MYSTERY
ROMANCE
SCIFI
WAR
WESTERN
}
Notice the bidirectional relationships:
Movie→ has aDirectorand list ofActorsDirector→ has a list ofMoviesActor→ has a list ofMovies
Step 2: Update the Models
Add relationship IDs to our models:
📁 src/main/java/com/example/moviedb/model/Movie.java
package com.example.moviedb.model;
import java.util.List;
public class Movie {
private String id;
private String title;
private int releaseYear;
private Genre genre;
private Double rating;
private Integer runtime;
private String plot;
private boolean inTheaters;
private String directorId;
private List<String> actorIds;
public Movie(String id, String title, int releaseYear, Genre genre,
Double rating, Integer runtime, String plot, boolean inTheaters,
String directorId, List<String> actorIds) {
this.id = id;
this.title = title;
this.releaseYear = releaseYear;
this.genre = genre;
this.rating = rating;
this.runtime = runtime;
this.plot = plot;
this.inTheaters = inTheaters;
this.directorId = directorId;
this.actorIds = actorIds;
}
// Getters
public String getId() { return id; }
public String getTitle() { return title; }
public int getReleaseYear() { return releaseYear; }
public Genre getGenre() { return genre; }
public Double getRating() { return rating; }
public Integer getRuntime() { return runtime; }
public String getPlot() { return plot; }
public boolean isInTheaters() { return inTheaters; }
public String getDirectorId() { return directorId; }
public List<String> getActorIds() { return actorIds; }
}
📁 src/main/java/com/example/moviedb/model/Director.java
package com.example.moviedb.model;
public class Director {
private String id;
private String name;
private Integer birthYear;
private String nationality;
public Director(String id, String name, Integer birthYear, String nationality) {
this.id = id;
this.name = name;
this.birthYear = birthYear;
this.nationality = nationality;
}
public String getId() { return id; }
public String getName() { return name; }
public Integer getBirthYear() { return birthYear; }
public String getNationality() { return nationality; }
}
Step 3: Update the Repositories
📁 src/main/java/com/example/moviedb/repository/MovieRepository.java
package com.example.moviedb.repository;
import com.example.moviedb.model.Genre;
import com.example.moviedb.model.Movie;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
public class MovieRepository {
private final List<Movie> movies = new ArrayList<>();
public MovieRepository() {
movies.add(new Movie("1", "The Shawshank Redemption", 1994, Genre.DRAMA, 9.3,
142, "Two imprisoned men bond over a number of years.", false,
"1", List.of("1", "2"))); // Darabont, Freeman & Robbins
movies.add(new Movie("2", "The Godfather", 1972, Genre.CRIME, 9.2,
175, "The aging patriarch of an organized crime dynasty.", false,
"2", List.of("3", "4"))); // Coppola, Brando & Pacino
movies.add(new Movie("3", "The Dark Knight", 2008, Genre.ACTION, 9.0,
152, "Batman faces the Joker, a criminal mastermind.", false,
"3", List.of("5", "6"))); // Nolan, Bale & Ledger
movies.add(new Movie("4", "Inception", 2010, Genre.SCIFI, 8.8,
148, "A thief who enters people's dreams.", false,
"3", List.of("7"))); // Nolan, DiCaprio
movies.add(new Movie("5", "Forrest Gump", 1994, Genre.DRAMA, 8.8,
142, "The story of a man with low IQ who achieved great things.", false,
"4", List.of("8"))); // Zemeckis, Hanks
movies.add(new Movie("6", "The Matrix", 1999, Genre.SCIFI, 8.7,
136, "A hacker discovers reality is a simulation.", false,
"5", List.of("9"))); // Wachowskis, Reeves
movies.add(new Movie("7", "Gladiator", 2000, Genre.ACTION, 8.5,
155, "A Roman General seeks revenge against the emperor.", false,
"6", List.of("10"))); // Ridley Scott, Crowe
movies.add(new Movie("8", "Interstellar", 2014, Genre.SCIFI, 8.6,
169, "Explorers travel through a wormhole in space.", false,
"3", List.of("7"))); // Nolan, DiCaprio (actually McConaughey, simplified)
}
public List<Movie> findAll() {
return new ArrayList<>(movies);
}
public Optional<Movie> findById(String id) {
return movies.stream()
.filter(m -> m.getId().equals(id))
.findFirst();
}
public List<Movie> findByGenre(Genre genre) {
return movies.stream()
.filter(m -> m.getGenre() == genre)
.toList();
}
public List<Movie> searchByTitle(String title) {
String lowerTitle = title.toLowerCase();
return movies.stream()
.filter(m -> m.getTitle().toLowerCase().contains(lowerTitle))
.toList();
}
public List<Movie> findByDirectorId(String directorId) {
return movies.stream()
.filter(m -> m.getDirectorId().equals(directorId))
.toList();
}
public List<Movie> findByActorId(String actorId) {
return movies.stream()
.filter(m -> m.getActorIds().contains(actorId))
.toList();
}
}
📁 src/main/java/com/example/moviedb/repository/DirectorRepository.java
package com.example.moviedb.repository;
import com.example.moviedb.model.Director;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Repository
public class DirectorRepository {
private final List<Director> directors = new ArrayList<>();
public DirectorRepository() {
directors.add(new Director("1", "Frank Darabont", 1959, "French-American"));
directors.add(new Director("2", "Francis Ford Coppola", 1939, "American"));
directors.add(new Director("3", "Christopher Nolan", 1970, "British-American"));
directors.add(new Director("4", "Robert Zemeckis", 1951, "American"));
directors.add(new Director("5", "Lana Wachowski", 1965, "American"));
directors.add(new Director("6", "Ridley Scott", 1937, "British"));
}
public List<Director> findAll() {
return new ArrayList<>(directors);
}
public Optional<Director> findById(String id) {
return directors.stream()
.filter(d -> d.getId().equals(id))
.findFirst();
}
}
Step 4: Create Field Resolvers with @SchemaMapping
Here's where the magic happens. We need to tell GraphQL how to resolve the director and actors fields on a Movie:
📁 src/main/java/com/example/moviedb/controller/MovieController.java
package com.example.moviedb.controller;
import com.example.moviedb.model.Actor;
import com.example.moviedb.model.Director;
import com.example.moviedb.model.Genre;
import com.example.moviedb.model.Movie;
import com.example.moviedb.repository.ActorRepository;
import com.example.moviedb.repository.DirectorRepository;
import com.example.moviedb.repository.MovieRepository;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
public class MovieController {
private final MovieRepository movieRepository;
private final DirectorRepository directorRepository;
private final ActorRepository actorRepository;
public MovieController(MovieRepository movieRepository,
DirectorRepository directorRepository,
ActorRepository actorRepository) {
this.movieRepository = movieRepository;
this.directorRepository = directorRepository;
this.actorRepository = actorRepository;
}
// ===== QUERY RESOLVERS =====
@QueryMapping
public Movie movie(@Argument String id) {
return movieRepository.findById(id).orElse(null);
}
@QueryMapping
public List<Movie> movies(@Argument Genre genre) {
if (genre != null) {
return movieRepository.findByGenre(genre);
}
return movieRepository.findAll();
}
@QueryMapping
public List<Movie> searchMovies(@Argument String title) {
return movieRepository.searchByTitle(title);
}
@QueryMapping
public Director director(@Argument String id) {
return directorRepository.findById(id).orElse(null);
}
@QueryMapping
public List<Director> directors() {
return directorRepository.findAll();
}
@QueryMapping
public Actor actor(@Argument String id) {
return actorRepository.findById(id).orElse(null);
}
@QueryMapping
public List<Actor> actors() {
return actorRepository.findAll();
}
// ===== FIELD RESOLVERS FOR MOVIE =====
@SchemaMapping(typeName = "Movie", field = "director")
public Director movieDirector(Movie movie) {
return directorRepository.findById(movie.getDirectorId()).orElse(null);
}
@SchemaMapping(typeName = "Movie", field = "actors")
public List<Actor> movieActors(Movie movie) {
return movie.getActorIds().stream()
.map(id -> actorRepository.findById(id).orElse(null))
.filter(actor -> actor != null)
.toList();
}
// ===== FIELD RESOLVERS FOR DIRECTOR =====
@SchemaMapping(typeName = "Director", field = "movies")
public List<Movie> directorMovies(Director director) {
return movieRepository.findByDirectorId(director.getId());
}
// ===== FIELD RESOLVERS FOR ACTOR =====
@SchemaMapping(typeName = "Actor", field = "movies")
public List<Movie> actorMovies(Actor actor) {
return movieRepository.findByActorId(actor.getId());
}
}
Understanding @SchemaMapping
┌─────────────────────────────────────────────────────────────────────┐
│ @SchemaMapping Explained │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ @SchemaMapping(typeName = "Movie", field = "director") │
│ │
│ This means: │
│ - When GraphQL resolves the "director" field │
│ - On a "Movie" type │
│ - Call this method │
│ │
│ The method receives the parent object (Movie) as first parameter │
│ │
│ Query: Resolution: │
│ ───────────────────────────────────────────────────────────────── │
│ movie(id: "1") { 1. Call movie("1") │
│ title ───────────▶ 2. Get Movie object │
│ director { ───────────▶ 3. Call movieDirector(movie) │
│ name ───────────▶ 4. Get name from Director │
│ } │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────┘
Step 5: Test Nested Queries
Restart your application and try these queries in GraphiQL:
Get Movie with Director
query {
movie(id: "1") {
title
releaseYear
director {
name
nationality
}
}
}
Response:
{
"data": {
"movie": {
"title": "The Shawshank Redemption",
"releaseYear": 1994,
"director": {
"name": "Frank Darabont",
"nationality": "French-American"
}
}
}
}
Get Movie with All Related Data
query {
movie(id: "3") {
title
rating
director {
name
birthYear
}
actors {
name
nationality
}
}
}
Get Director with Their Movies
query {
director(id: "3") {
name
movies {
title
releaseYear
rating
}
}
}
Deep Nesting (Be Careful!)
GraphQL allows infinite nesting. This is valid:
query {
movie(id: "1") {
director {
movies {
director {
movies {
title
}
}
}
}
}
}
Deep nesting can cause performance issues. We'll address this with DataLoader in Class 5 and query complexity limits in Class 8.
Understanding the Resolver Chain
Let's trace how GraphQL resolves a nested query:
┌─────────────────────────────────────────────────────────────────────┐
│ RESOLVER CHAIN │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Query: movie(id: "3") { title, director { name, movies { title } }}│
│ │
│ Step 1: Resolve Query.movie │
│ └─▶ movie("3") returns Movie(id=3, title="Dark Knight"...) │
│ │
│ Step 2: Resolve Movie.title │
│ └─▶ Direct field access: "The Dark Knight" │
│ │
│ Step 3: Resolve Movie.director │
│ └─▶ movieDirector(movie) returns Director(id=3, "Nolan"...)│
│ │
│ Step 4: Resolve Director.name │
│ └─▶ Direct field access: "Christopher Nolan" │
│ │
│ Step 5: Resolve Director.movies │
│ └─▶ directorMovies(director) returns [Movie, Movie, Movie] │
│ │
│ Step 6: Resolve Movie.title for each movie in list │
│ └─▶ "The Dark Knight", "Inception", "Interstellar" │
│ │
└─────────────────────────────────────────────────────────────────────┘
Alternative: Separate Controller Classes
For better organization, you can split resolvers by type:
📁 src/main/java/com/example/moviedb/controller/DirectorController.java
package com.example.moviedb.controller;
import com.example.moviedb.model.Director;
import com.example.moviedb.model.Movie;
import com.example.moviedb.repository.DirectorRepository;
import com.example.moviedb.repository.MovieRepository;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SchemaMapping;
import org.springframework.stereotype.Controller;
import java.util.List;
@Controller
public class DirectorController {
private final DirectorRepository directorRepository;
private final MovieRepository movieRepository;
public DirectorController(DirectorRepository directorRepository,
MovieRepository movieRepository) {
this.directorRepository = directorRepository;
this.movieRepository = movieRepository;
}
@QueryMapping
public Director director(@Argument String id) {
return directorRepository.findById(id).orElse(null);
}
@QueryMapping
public List<Director> directors() {
return directorRepository.findAll();
}
@SchemaMapping(typeName = "Director", field = "movies")
public List<Movie> movies(Director director) {
return movieRepository.findByDirectorId(director.getId());
}
}
The @SchemaMapping Shorthand
When the method name matches the field name, you can use a shorter syntax:
// These are equivalent:
@SchemaMapping(typeName = "Director", field = "movies")
public List<Movie> moviesResolver(Director director) { ... }
@SchemaMapping(typeName = "Director")
public List<Movie> movies(Director director) { ... }
Exercises
Exercise 1: Add Awards to Actors
Add a list of awards to actors:
type Actor {
# ... existing fields
awards: [String!]!
}
Update the Actor model and repository with award data.
Exercise 2: Add Computed Fields
Add a computed field isClassic to Movie that returns true if the movie is older than 25 years:
type Movie {
# ... existing fields
isClassic: Boolean!
}
Solution
@SchemaMapping(typeName = "Movie", field = "isClassic")
public boolean isClassic(Movie movie) {
int currentYear = java.time.Year.now().getValue();
return (currentYear - movie.getReleaseYear()) > 25;
}
Exercise 3: Multiple Genres per Movie
Change the schema to support multiple genres per movie. How would you update the model and filtering?
Common Mistakes
Mistake 1: Forgetting the Parent Parameter
// ❌ Wrong - no way to know which movie's director
@SchemaMapping(typeName = "Movie", field = "director")
public Director director() {
return ???; // Which movie?
}
// ✅ Correct - parent object is passed
@SchemaMapping(typeName = "Movie", field = "director")
public Director director(Movie movie) {
return directorRepository.findById(movie.getDirectorId()).orElse(null);
}
Mistake 2: Returning Wrong Type
// ❌ Wrong - schema says director: Director!
@SchemaMapping(typeName = "Movie", field = "director")
public String director(Movie movie) {
return movie.getDirectorId(); // Returns String, not Director
}
Mistake 3: N+1 Problem (Preview)
// This creates N+1 queries - we'll fix this in Class 5
@SchemaMapping(typeName = "Movie", field = "actors")
public List<Actor> actors(Movie movie) {
// For 10 movies, this runs 10 times!
return movie.getActorIds().stream()
.map(id -> actorRepository.findById(id).orElse(null))
.toList();
}
Summary
In this class, you learned:
✅ How to create relationships between GraphQL types
✅ Using @SchemaMapping for field resolvers
✅ The parent object is passed as the first parameter
✅ How GraphQL resolves nested queries step by step
✅ Bidirectional relationships between types
What's Next?
In Class 4: Mutations, we'll learn:
- Creating, updating, and deleting data
- Input types for complex arguments
- Mutation best practices
- Return types and error handling
Time to make our API read-write!