Skip to main content

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 @SchemaMapping for 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 a Director and list of Actors
  • Director → has a list of Movies
  • Actor → has a list of Movies

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"
}
}
}
}
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
}
}
}
}
}
}
Circular References

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!