Risolutori di Mutations in GraphQL: Come Gestire le Operazioni di Scrittura
In GraphQL, le mutations sono utilizzate per eseguire operazioni di scrittura, come la creazione, l’aggiornamento o la cancellazione di dati. A differenza delle query, che si occupano di leggere i dati, le mutations modificano lo stato delle risorse nel server. I risolutori per le mutations gestiscono la logica necessaria per eseguire queste operazioni. In questo articolo esploreremo come implementare risolutori di mutations in GraphQL, come gestire argomenti complessi, validare dati in ingresso e migliorare le performance delle operazioni di scrittura.
Cos’è una Mutation in GraphQL?
Una mutation è una richiesta che altera i dati memorizzati nel backend. In un’applicazione tipica, potrebbe essere usata per:
- Creare nuovi record (es. creare un nuovo utente o post).
- Aggiornare dati esistenti (es. aggiornare il profilo di un utente).
- Cancellare record (es. rimuovere un post o un commento).
Le mutations funzionano in modo simile alle query, ma ogni volta che una mutation viene eseguita, essa può alterare lo stato del sistema. Le mutations in GraphQL sono rappresentate come un tipo separato nello schema.
Esempio di Schema con Mutation
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean
}
input CreateUserInput {
name: String!
email: String!
password: String!
}
input UpdateUserInput {
name: String
email: String
password: String
}
type User {
id: ID!
name: String!
email: String!
}
In questo schema, abbiamo tre mutations principali: createUser
, updateUser
e deleteUser
. Le mutations accettano input definiti da tipi personalizzati per gestire i dati in ingresso e restituire il tipo User
o Boolean
.
Come Funzionano i Risolutori di Mutations
Un risolutore di mutation è una funzione che gestisce la logica di backend per una mutation. Ogni mutation ha un risolutore associato che accetta gli argomenti forniti dal client e interagisce con il database o altre risorse per eseguire l’operazione richiesta.
Struttura di Base di un Risolutore di Mutation
I risolutori di mutation funzionano in modo simile ai risolutori di query, ma il loro obiettivo principale è modificare lo stato dei dati.
const resolvers = {
Mutation: {
createUser: async (parent, { input }, context) => {
const newUser = await context.db.createUser(input);
return newUser;
},
updateUser: async (parent, { id, input }, context) => {
const updatedUser = await context.db.updateUser(id, input);
return updatedUser;
},
deleteUser: async (parent, { id }, context) => {
const result = await context.db.deleteUser(id);
return result ? true : false;
},
},
};
In questo esempio, ci sono tre risolutori di mutation che eseguono operazioni asincrone (con async/await
) per creare, aggiornare e cancellare un utente.
Esempi Pratici di Risolutori di Mutations
1. Creare una Risorsa
La mutation createUser
crea un nuovo utente nel database.
Implementazione di createUser
const resolvers = {
Mutation: {
createUser: async (parent, { input }, context) => {
const { name, email, password } = input;
// Validazione dei dati
if (!email.includes("@")) {
throw new Error("Email non valida");
}
// Creazione di un nuovo utente nel database
const newUser = await context.db.createUser({
name,
email,
password: await hashPassword(password), // Esegui hash della password
});
return newUser;
},
},
};
In questo esempio:
- Viene eseguita la validazione dell’email.
- La password viene hashed prima di essere salvata nel database.
- Viene restituito l’utente appena creato.
2. Aggiornare una Risorsa
La mutation updateUser
consente di aggiornare i dettagli di un utente esistente.
Implementazione di updateUser
const resolvers = {
Mutation: {
updateUser: async (parent, { id, input }, context) => {
// Verifica se l'utente esiste
const user = await context.db.getUserById(id);
if (!user) {
throw new Error("Utente non trovato");
}
// Aggiorna i dati dell'utente nel database
const updatedUser = await context.db.updateUser(id, input);
return updatedUser;
},
},
};
Questo risolutore controlla prima se l’utente esiste e, se trovato, aggiorna i campi forniti nel database.
3. Cancellare una Risorsa
La mutation deleteUser
consente di cancellare un utente dal database.
Implementazione di deleteUser
const resolvers = {
Mutation: {
deleteUser: async (parent, { id }, context) => {
const user = await context.db.getUserById(id);
if (!user) {
throw new Error("Utente non trovato");
}
const result = await context.db.deleteUser(id);
return result ? true : false;
},
},
};
In questo esempio:
- Si verifica se l’utente esiste prima di tentare di cancellarlo.
- Se l’utente viene cancellato con successo, viene restituito
true
, altrimentifalse
.
Best Practices per Risolutori di Mutations
1. Validazione degli Input
Prima di eseguire una mutation, è essenziale validare gli input forniti dal client. Le mutations che creano o aggiornano risorse dovrebbero assicurarsi che i dati siano nel formato corretto e rispettino le regole di business dell’applicazione.
Esempio di Validazione
const resolvers = {
Mutation: {
createUser: async (parent, { input }, context) => {
const { email } = input;
// Controllo formato email
if (!email.includes("@")) {
throw new Error("Email non valida");
}
// Altre validazioni...
// Creazione dell'utente
return await context.db.createUser(input);
},
},
};
2. Gestione degli Errori
La gestione degli errori è fondamentale nei risolutori di mutation. In caso di errore, come un dato non valido o un problema con il database, dovresti restituire messaggi di errore chiari e significativi al client.
Esempio di Gestione degli Errori
const resolvers = {
Mutation: {
updateUser: async (parent, { id, input }, context) => {
try {
const user = await context.db.getUserById(id);
if (!user) {
throw new Error("Utente non trovato");
}
return await context.db.updateUser(id, input);
} catch (error) {
throw new Error(
`Errore durante l'aggiornamento dell'utente: ${error.message}`
);
}
},
},
};
3. Mutazioni Ottimistiche
Le mutazioni ottimistiche permettono di aggiornare immediatamente l’interfaccia utente prima che la risposta del server sia ricevuta. Questo migliora l’esperienza utente, specialmente in applicazioni con latenza elevata. Relay Modern e Apollo Client supportano le mutazioni ottimistiche.
Esempio con Apollo Client (Mutation Ottimistica)
import { gql, useMutation } from "@apollo/client";
const UPDATE_USER = gql`
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
}
}
`;
const [updateUser] = useMutation(UPDATE_USER);
const handleUpdate = (id, input) => {
updateUser({
variables: { id, input },
optimisticResponse: {
updateUser: {
id,
name: input.name,
email: input.email,
},
},
});
};
In questo esempio, Apollo Client aggiorna l’interfaccia utente in modo ottimistico prima che la risposta dal server venga confermata.
4. Gestione delle Transazioni
Se una mutation esegue piĂą operazioni che devono essere eseguite
in modo atomico, è utile implementare transazioni per garantire che o tutte le operazioni vengano completate con successo o nessuna di esse venga applicata.
Esempio di Transazione
const resolvers = {
Mutation: {
createOrder: async (parent, { input }, context) => {
const transaction = await context.db.startTransaction();
try {
const order = await context.db.createOrder(input.orderDetails, {
transaction,
});
await context.db.updateInventory(input.products, { transaction });
await transaction.commit(); // Conferma la transazione
return order;
} catch (error) {
await transaction.rollback(); // Annulla la transazione in caso di errore
throw new Error("Errore durante la creazione dell'ordine");
}
},
},
};
In questo esempio, la creazione di un ordine e l’aggiornamento dell’inventario sono eseguiti in una transazione. Se uno dei due fallisce, l’intera operazione viene annullata.
Conclusione
I risolutori di mutations in GraphQL sono fondamentali per gestire le operazioni di scrittura, come la creazione, l’aggiornamento e la cancellazione di dati. Implementare risolutori di mutation efficienti e sicuri richiede la gestione accurata degli input, una solida gestione degli errori e l’uso di tecniche avanzate come le mutazioni ottimistiche e le transazioni. Seguendo le best practices per la validazione e la sicurezza, puoi garantire che le tue mutations siano robuste, scalabili e facili da mantenere, offrendo un’esperienza utente fluida e reattiva.