Class 4: Mutations
Duration: 30 minutes Difficulty: Intermediate Prerequisites: Completed Classes 1-3
What You'll Learn
By the end of this class, you will:
- Create mutations for adding new data
- Use input types for complex arguments
- Implement update and delete operations
- Return meaningful responses from mutations
- Follow mutation best practices
Queries vs Mutations
In GraphQL:
- Queries = Read operations (GET in REST)
- Mutations = Write operations (POST, PUT, DELETE in REST)
┌─────────────────────────────────────────────────────────────────────┐
│ QUERIES vs MUTATIONS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ QUERIES MUTATIONS │
│ ──────────────────────────────────────────────────────────────── │
│ - Read data - Write data │
│ - Run in parallel - Run sequentially │
│ - Idempotent (safe to repeat) - May have side effects │
│ - Cacheable - Not cacheable │
│ │
│ query { mutation { │
│ movies { title } createMovie(...) { id } │
│ } } │
│ │
└─────────────────────────────────────────────────────────────────────┘
Step 1: Define Input Types
Input types are special types used for mutation arguments. They're like regular types but prefixed with input:
📁 Update src/main/resources/graphql/schema.graphqls:
# Add after your Query type
type Mutation {
"Create a new movie"
createMovie(input: CreateMovieInput!): Movie!
"Update an existing movie"
updateMovie(id: ID!, input: UpdateMovieInput!): Movie
"Delete a movie"
deleteMovie(id: ID!): DeleteResponse!
"Create a new actor"
createActor(input: CreateActorInput!): Actor!
"Create a new director"
createDirector(input: CreateDirectorInput!): Director!
"Add an actor to a movie"
addActorToMovie(movieId: ID!, actorId: ID!): Movie!
"Remove an actor from a movie"
removeActorFromMovie(movieId: ID!, actorId: ID!): Movie!
}
"""
Input for creating a new movie.
All required fields must be provided.
"""
input CreateMovieInput {
title: String!
releaseYear: Int!
genre: Genre!
rating: Float
runtime: Int
plot: String
directorId: ID!
actorIds: [ID!]
}
"""
Input for updating a movie.
All fields are optional - only provided fields will be updated.
"""
input UpdateMovieInput {
title: String
releaseYear: Int
genre: Genre
rating: Float
runtime: Int
plot: String
inTheaters: Boolean
}
input CreateActorInput {
name: String!
birthYear: Int
nationality: String
}
input CreateDirectorInput {
name: String!
birthYear: Int
nationality: String
}
"""
Standard response for delete operations.
"""
type DeleteResponse {
success: Boolean!
message: String!
deletedId: ID
}
Input Types vs Regular Types
┌─────────────────────────────────────────────────────────────────────┐
│ INPUT vs OUTPUT TYPES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ type Movie { vs input CreateMovieInput { │
│ id: ID! title: String! │
│ title: String! releaseYear: Int! │
│ director: Director! genre: Genre! │
│ actors: [Actor!]! directorId: ID! ← Just ID │
│ } actorIds: [ID!] ← Just IDs │
│ } │
│ │
│ - Used in responses - Used in arguments │
│ - Can have resolvers - No resolvers │
│ - Can reference other types - Only references by ID │
│ │
└─────────────────────────────────────────────────────────────────────┘
Step 2: Create Input DTOs
Create Java classes to match your input types:
📁 src/main/java/com/example/moviedb/dto/CreateMovieInput.java
package com.example.moviedb.dto;
import com.example.moviedb.model.Genre;
import java.util.List;
public class CreateMovieInput {
private String title;
private int releaseYear;
private Genre genre;
private Double rating;
private Integer runtime;
private String plot;
private String directorId;
private List<String> actorIds;
// Getters and setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public int getReleaseYear() { return releaseYear; }
public void setReleaseYear(int releaseYear) { this.releaseYear = releaseYear; }
public Genre getGenre() { return genre; }
public void setGenre(Genre genre) { this.genre = genre; }
public Double getRating() { return rating; }
public void setRating(Double rating) { this.rating = rating; }
public Integer getRuntime() { return runtime; }
public void setRuntime(Integer runtime) { this.runtime = runtime; }
public String getPlot() { return plot; }
public void setPlot(String plot) { this.plot = plot; }
public String getDirectorId() { return directorId; }
public void setDirectorId(String directorId) { this.directorId = directorId; }
public List<String> getActorIds() { return actorIds; }
public void setActorIds(List<String> actorIds) { this.actorIds = actorIds; }
}
📁 src/main/java/com/example/moviedb/dto/UpdateMovieInput.java
package com.example.moviedb.dto;
import com.example.moviedb.model.Genre;
public class UpdateMovieInput {
private String title;
private Integer releaseYear;
private Genre genre;
private Double rating;
private Integer runtime;
private String plot;
private Boolean inTheaters;
// All nullable - only update what's provided
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public Integer getReleaseYear() { return releaseYear; }
public void setReleaseYear(Integer releaseYear) { this.releaseYear = releaseYear; }
public Genre getGenre() { return genre; }
public void setGenre(Genre genre) { this.genre = genre; }
public Double getRating() { return rating; }
public void setRating(Double rating) { this.rating = rating; }
public Integer getRuntime() { return runtime; }
public void setRuntime(Integer runtime) { this.runtime = runtime; }
public String getPlot() { return plot; }
public void setPlot(String plot) { this.plot = plot; }
public Boolean getInTheaters() { return inTheaters; }
public void setInTheaters(Boolean inTheaters) { this.inTheaters = inTheaters; }
}
📁 src/main/java/com/example/moviedb/dto/DeleteResponse.java
package com.example.moviedb.dto;
public class DeleteResponse {
private boolean success;
private String message;
private String deletedId;
public DeleteResponse(boolean success, String message, String deletedId) {
this.success = success;
this.message = message;
this.deletedId = deletedId;
}
public static DeleteResponse success(String id) {
return new DeleteResponse(true, "Successfully deleted", id);
}
public static DeleteResponse notFound(String id) {
return new DeleteResponse(false, "Item not found with id: " + id, null);
}
public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public String getDeletedId() { return deletedId; }
}
📁 src/main/java/com/example/moviedb/dto/CreateActorInput.java
package com.example.moviedb.dto;
public class CreateActorInput {
private String name;
private Integer birthYear;
private String nationality;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getBirthYear() { return birthYear; }
public void setBirthYear(Integer birthYear) { this.birthYear = birthYear; }
public String getNationality() { return nationality; }
public void setNationality(String nationality) { this.nationality = nationality; }
}
📁 src/main/java/com/example/moviedb/dto/CreateDirectorInput.java
package com.example.moviedb.dto;
public class CreateDirectorInput {
private String name;
private Integer birthYear;
private String nationality;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getBirthYear() { return birthYear; }
public void setBirthYear(Integer birthYear) { this.birthYear = birthYear; }
public String getNationality() { return nationality; }
public void setNationality(String nationality) { this.nationality = nationality; }
}
Step 3: Update Repositories to Support Mutations
📁 src/main/java/com/example/moviedb/repository/MovieRepository.java
Add these methods:
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;
import java.util.UUID;
@Repository
public class MovieRepository {
private final List<Movie> movies = new ArrayList<>();
private int idCounter = 100; // Start after sample data
public MovieRepository() {
// ... existing sample data initialization
}
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();
}
// ===== NEW MUTATION METHODS =====
public Movie save(Movie movie) {
if (movie.getId() == null) {
// Create new movie with generated ID
Movie newMovie = new Movie(
String.valueOf(++idCounter),
movie.getTitle(),
movie.getReleaseYear(),
movie.getGenre(),
movie.getRating(),
movie.getRuntime(),
movie.getPlot(),
movie.isInTheaters(),
movie.getDirectorId(),
movie.getActorIds()
);
movies.add(newMovie);
return newMovie;
} else {
// Update existing - remove old and add new
movies.removeIf(m -> m.getId().equals(movie.getId()));
movies.add(movie);
return movie;
}
}
public boolean delete(String id) {
return movies.removeIf(m -> m.getId().equals(id));
}
}
📁 src/main/java/com/example/moviedb/repository/ActorRepository.java
Add save method:
private int idCounter = 100;
public Actor save(Actor actor) {
if (actor.getId() == null) {
Actor newActor = new Actor(
String.valueOf(++idCounter),
actor.getName(),
actor.getBirthYear(),
actor.getNationality()
);
actors.add(newActor);
return newActor;
}
return actor;
}
📁 src/main/java/com/example/moviedb/repository/DirectorRepository.java
Add save method similarly.
Step 4: Create the Mutation Controller
📁 src/main/java/com/example/moviedb/controller/MutationController.java
package com.example.moviedb.controller;
import com.example.moviedb.dto.*;
import com.example.moviedb.model.Actor;
import com.example.moviedb.model.Director;
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.MutationMapping;
import org.springframework.stereotype.Controller;
import java.util.ArrayList;
import java.util.List;
@Controller
public class MutationController {
private final MovieRepository movieRepository;
private final ActorRepository actorRepository;
private final DirectorRepository directorRepository;
public MutationController(MovieRepository movieRepository,
ActorRepository actorRepository,
DirectorRepository directorRepository) {
this.movieRepository = movieRepository;
this.actorRepository = actorRepository;
this.directorRepository = directorRepository;
}
@MutationMapping
public Movie createMovie(@Argument CreateMovieInput input) {
// Validate director exists
directorRepository.findById(input.getDirectorId())
.orElseThrow(() -> new IllegalArgumentException(
"Director not found: " + input.getDirectorId()));
// Validate actors exist
if (input.getActorIds() != null) {
for (String actorId : input.getActorIds()) {
actorRepository.findById(actorId)
.orElseThrow(() -> new IllegalArgumentException(
"Actor not found: " + actorId));
}
}
Movie movie = new Movie(
null, // ID will be generated
input.getTitle(),
input.getReleaseYear(),
input.getGenre(),
input.getRating(),
input.getRuntime(),
input.getPlot(),
false, // Not in theaters by default
input.getDirectorId(),
input.getActorIds() != null ? input.getActorIds() : List.of()
);
return movieRepository.save(movie);
}
@MutationMapping
public Movie updateMovie(@Argument String id, @Argument UpdateMovieInput input) {
Movie existing = movieRepository.findById(id).orElse(null);
if (existing == null) {
return null;
}
// Apply partial updates - only update non-null fields
Movie updated = new Movie(
existing.getId(),
input.getTitle() != null ? input.getTitle() : existing.getTitle(),
input.getReleaseYear() != null ? input.getReleaseYear() : existing.getReleaseYear(),
input.getGenre() != null ? input.getGenre() : existing.getGenre(),
input.getRating() != null ? input.getRating() : existing.getRating(),
input.getRuntime() != null ? input.getRuntime() : existing.getRuntime(),
input.getPlot() != null ? input.getPlot() : existing.getPlot(),
input.getInTheaters() != null ? input.getInTheaters() : existing.isInTheaters(),
existing.getDirectorId(),
existing.getActorIds()
);
return movieRepository.save(updated);
}
@MutationMapping
public DeleteResponse deleteMovie(@Argument String id) {
boolean deleted = movieRepository.delete(id);
if (deleted) {
return DeleteResponse.success(id);
}
return DeleteResponse.notFound(id);
}
@MutationMapping
public Actor createActor(@Argument CreateActorInput input) {
Actor actor = new Actor(
null,
input.getName(),
input.getBirthYear(),
input.getNationality()
);
return actorRepository.save(actor);
}
@MutationMapping
public Director createDirector(@Argument CreateDirectorInput input) {
Director director = new Director(
null,
input.getName(),
input.getBirthYear(),
input.getNationality()
);
return directorRepository.save(director);
}
@MutationMapping
public Movie addActorToMovie(@Argument String movieId, @Argument String actorId) {
Movie movie = movieRepository.findById(movieId)
.orElseThrow(() -> new IllegalArgumentException("Movie not found"));
actorRepository.findById(actorId)
.orElseThrow(() -> new IllegalArgumentException("Actor not found"));
List<String> actorIds = new ArrayList<>(movie.getActorIds());
if (!actorIds.contains(actorId)) {
actorIds.add(actorId);
}
Movie updated = new Movie(
movie.getId(),
movie.getTitle(),
movie.getReleaseYear(),
movie.getGenre(),
movie.getRating(),
movie.getRuntime(),
movie.getPlot(),
movie.isInTheaters(),
movie.getDirectorId(),
actorIds
);
return movieRepository.save(updated);
}
@MutationMapping
public Movie removeActorFromMovie(@Argument String movieId, @Argument String actorId) {
Movie movie = movieRepository.findById(movieId)
.orElseThrow(() -> new IllegalArgumentException("Movie not found"));
List<String> actorIds = new ArrayList<>(movie.getActorIds());
actorIds.remove(actorId);
Movie updated = new Movie(
movie.getId(),
movie.getTitle(),
movie.getReleaseYear(),
movie.getGenre(),
movie.getRating(),
movie.getRuntime(),
movie.getPlot(),
movie.isInTheaters(),
movie.getDirectorId(),
actorIds
);
return movieRepository.save(updated);
}
}
Step 5: Test Your Mutations
Restart your application and try these in GraphiQL:
Create a New Movie
mutation {
createMovie(input: {
title: "The Prestige"
releaseYear: 2006
genre: MYSTERY
rating: 8.5
runtime: 130
plot: "Two rival magicians engage in a bitter competition"
directorId: "3"
actorIds: ["5", "7"]
}) {
id
title
director {
name
}
actors {
name
}
}
}
Update a Movie
mutation {
updateMovie(id: "1", input: {
rating: 9.5
inTheaters: true
}) {
id
title
rating
inTheaters
}
}
Delete a Movie
mutation {
deleteMovie(id: "101") {
success
message
deletedId
}
}
Create a New Actor
mutation {
createActor(input: {
name: "Scarlett Johansson"
birthYear: 1984
nationality: "American"
}) {
id
name
}
}
Add Actor to Movie
mutation {
addActorToMovie(movieId: "1", actorId: "101") {
title
actors {
name
}
}
}
Using Variables (Best Practice)
In production, you should use variables instead of inline values:
mutation CreateMovie($input: CreateMovieInput!) {
createMovie(input: $input) {
id
title
}
}
Variables (in the Variables panel of GraphiQL):
{
"input": {
"title": "Tenet",
"releaseYear": 2020,
"genre": "SCIFI",
"rating": 7.4,
"directorId": "3"
}
}
Mutation Best Practices
1. Use Input Types for Complex Arguments
# ❌ Bad - Too many arguments
mutation {
createMovie(
title: "...",
releaseYear: 2020,
genre: ACTION,
rating: 8.0,
runtime: 120,
plot: "...",
directorId: "1"
) { id }
}
# ✅ Good - Single input object
mutation {
createMovie(input: {
title: "...",
releaseYear: 2020,
...
}) { id }
}
2. Return the Modified Object
# ❌ Bad - Returns just an ID
type Mutation {
createMovie(input: CreateMovieInput!): ID!
}
# ✅ Good - Returns the full object
type Mutation {
createMovie(input: CreateMovieInput!): Movie!
}
3. Use Meaningful Response Types for Deletes
# ❌ Bad - Just a boolean
type Mutation {
deleteMovie(id: ID!): Boolean!
}
# ✅ Good - Detailed response
type Mutation {
deleteMovie(id: ID!): DeleteResponse!
}
type DeleteResponse {
success: Boolean!
message: String!
deletedId: ID
}
4. Validate Input
@MutationMapping
public Movie createMovie(@Argument CreateMovieInput input) {
// Validate year is reasonable
int currentYear = Year.now().getValue();
if (input.getReleaseYear() < 1888 || input.getReleaseYear() > currentYear + 5) {
throw new IllegalArgumentException("Invalid release year");
}
// Validate rating range
if (input.getRating() != null &&
(input.getRating() < 0 || input.getRating() > 10)) {
throw new IllegalArgumentException("Rating must be between 0 and 10");
}
// ... create movie
}
Exercises
Exercise 1: Add updateActor Mutation
Create a mutation to update actor details:
type Mutation {
updateActor(id: ID!, input: UpdateActorInput!): Actor
}
input UpdateActorInput {
name: String
birthYear: Int
nationality: String
}
Exercise 2: Add Rate Movie Mutation
Create a simple mutation for rating a movie:
type Mutation {
rateMovie(id: ID!, rating: Float!): Movie!
}
Validate that rating is between 0 and 10.
Exercise 3: Batch Create
Create a mutation that creates multiple movies at once:
type Mutation {
createMovies(inputs: [CreateMovieInput!]!): [Movie!]!
}
Summary
In this class, you learned:
✅ The difference between queries (reads) and mutations (writes)
✅ How to define input types for structured arguments
✅ Using @MutationMapping to handle mutations
✅ Creating, updating, and deleting data
✅ Returning meaningful responses from mutations
✅ Input validation best practices
What's Next?
In Class 5: Relationships & DataLoader, we'll tackle:
- The N+1 problem in GraphQL
- Efficient data loading with DataLoader
- Batching and caching strategies
- @BatchMapping for optimized queries
This is where performance optimization gets serious!