Implementare le mutazioni di Data Connect

Firebase Data Connect ti consente di creare connettori per le tue istanze PostgreSQL gestite con Google Cloud SQL. Questi connettori sono combinazioni di query e mutazioni per utilizzare i dati dello schema.

La Guida introduttiva ha introdotto uno schema di app di recensioni di film per PostgreSQL.

Questa guida ha anche introdotto operazioni amministrative sia implementabili che ad hoc, incluse le mutazioni.

  • Le mutazioni implementabili sono quelle che implementi per chiamare dalle app client in un connettore, con gli endpoint API che definisci. Data Connect integra l'autenticazione e l'autorizzazione in queste mutazioni e genera SDK client in base alla tua API.
  • Le mutazioni amministrative ad hoc vengono eseguite da ambienti privilegiati per compilare e gestire le tabelle. Puoi crearli ed eseguirli nella console Firebase, da ambienti privilegiati utilizzando Firebase Admin SDK e in ambienti di sviluppo locali utilizzando la nostra estensione VS Code Data Connect.

Questa guida esamina più da vicino le mutazioni implementabili.

Caratteristiche delle mutazioni di Data Connect

Data Connect ti consente di eseguire mutazioni di base in tutti i modi che ti aspetteresti dato un database PostgreSQL:

  • Eseguire operazioni CRUD
  • Gestire operazioni in più passaggi con le transazioni

Tuttavia, con le estensioni di Data Connect a GraphQL, puoi implementare mutazioni avanzate per app più veloci ed efficienti:

  • Utilizza gli scalari chiave restituiti da molte operazioni per semplificare le operazioni ripetute sui record
  • Utilizza i valori del server per compilare i dati con le operazioni fornite dal server.
  • Esegui query nel corso di operazioni di mutazione in più passaggi per cercare dati, risparmiando righe di codice e round trip al server.

Utilizzare i campi generati per implementare le mutazioni

Le operazioni Data Connect estenderanno un insieme di campi generati automaticamente Data Connect in base ai tipi e alle relazioni tra i tipi nello schema. Questi campi vengono generati dagli strumenti locali ogni volta che modifichi lo schema.

Puoi utilizzare i campi generati per implementare le mutazioni, dalla creazione, all'aggiornamento e all'eliminazione di singoli record in singole tabelle, fino ad aggiornamenti multitabella più complessi.

Supponiamo che lo schema contenga un tipo Movie e un tipo Actor associato. Data Connect genera i campi movie_insert, movie_update, movie_delete e altri ancora.

Mutazione con il campo
movie_insert

Il campo movie_insert rappresenta una mutazione per creare un singolo record nella tabella Movie.

Utilizza questo campo per creare un singolo filmato.

mutation CreateMovie($data: Movie_Data!) {
  movie_insert(data: $data) { key }
}

Mutazione con il campo
movie_update

Il campo movie_update rappresenta una mutazione per aggiornare un singolo record nella tabella Movie.

Utilizza questo campo per aggiornare un singolo film in base alla chiave.

mutation UpdateMovie($myKey: Movie_Key!, $data: Movie_Data!) {
  movie_update(key: $myKey, data: $data) { key }
}

Mutazione con il campo
movie_delete

Il campo movie_delete rappresenta una mutazione per eliminare un singolo record nella tabella Movie.

Utilizza questo campo per eliminare un singolo film in base alla chiave.

  mutation DeleteMovie($myKey: Movie_Key!) {
    movie_delete(key: $myKey) { key }
  }

Elementi essenziali di una mutazione

Le mutazioni Data Connect sono mutazioni GraphQL con estensioni Data Connect. Come per una normale mutazione GraphQL, puoi definire un nome di operazione e un elenco di variabili GraphQL.

Data Connect estende le query GraphQL con direttive personalizzate come @auth e @transaction.

Pertanto, la seguente mutazione ha:

  • Definizione del tipo mutation
  • Il nome di un'operazione (mutazione) SignUp
  • Un singolo argomento dell'operazione $username
  • Una singola direttiva, @auth
  • Un singolo campo user_insert.
mutation SignUp($username: String!) @auth(level: USER) {
  user_insert(data: {
    id_expr: "auth.uid"
    username: $username
  })
}

Ogni argomento di mutazione richiede una dichiarazione di tipo, un tipo integrato come String o un tipo personalizzato definito dallo schema come Movie.

Scrivere mutazioni di base

Puoi iniziare a scrivere mutazioni per creare, aggiornare ed eliminare singoli record dal tuo database.

Crea

Eseguiamo creazioni di base.

# Create a movie based on user input
mutation CreateMovie($title: String!, $releaseYear: Int!, $genre: String!, $rating: Int!) {
  movie_insert(data: {
    title: $title
    releaseYear: $releaseYear
    genre: $genre
    rating: $rating
  })
}

# Create a movie with default values
mutation CreateMovie2 {
  movie_insert(data: {
    title: "Sherlock Holmes"
    releaseYear: 2009
    genre: "Mystery"
    rating: 5
  })
}

o un upsert.

# Movie upsert using combination of variables and literals
mutation UpsertMovie($title: String!) {
  movie_upsert(data: {
    title: $title
    releaseYear: 2009
    genre: "Mystery"
    rating: 5
    genre: "Mystery/Thriller"
  })
}

Eseguire gli aggiornamenti

Ecco gli aggiornamenti. I produttori e i registi sperano sicuramente che queste valutazioni medie siano in linea con le tendenze.

Il campo movie_update contiene un argomento id previsto per identificare un record e un campo data che puoi utilizzare per impostare i valori in questo aggiornamento.

mutation UpdateMovie(
  $id: UUID!,
  $genre: String!,
  $rating: Int!,
  $description: String!
) {
  movie_update(id: $id,
    data: {
      genre: $genre
      rating: $rating
      description: $description
    })
}

Per eseguire più aggiornamenti, utilizza il campo movie_updateMany.

# Multiple updates (increase all ratings of a genre)
mutation IncreaseRatingForGenre($genre: String!, $rating: Int!) {
  movie_updateMany(
    where: { genre: { eq: $genre } },
    data:
      {
        rating: $rating
      })
}

Utilizza le operazioni di incremento, decremento, aggiunta e anteposizione con _update

Nelle mutazioni _update e _updateMany puoi impostare esplicitamente i valori in data:, ma spesso è più sensato applicare un operatore come increment per aggiornare i valori.

Per modificare l'esempio di aggiornamento precedente, supponiamo che tu voglia incrementare la valutazione di un determinato film. Puoi utilizzare la sintassi rating_update con l'operatore inc.

mutation UpdateMovie(
  $id: UUID!,
  $ratingIncrement: Int!
) {
  movie_update(id: $id, data: {
    rating_update: {
      inc: $ratingIncrement
    }
  })
}

Data Connect supporta i seguenti operatori per gli aggiornamenti dei campi:

  • inc per incrementare i tipi di dati Int, Int64, Float, Date e Timestamp
  • dec per decrementare i tipi di dati Int, Int64, Float, Date e Timestamp

Per gli elenchi, puoi anche eseguire l'aggiornamento con singoli valori o elenchi di valori utilizzando:

  • add per aggiungere elementi se non sono già presenti ai tipi di elenchi, ad eccezione degli elenchi vettoriali
  • remove per rimuovere tutti gli elementi, se presenti, dai tipi di elenchi, ad eccezione degli elenchi vettoriali
  • append per aggiungere elementi ai tipi di elenchi, ad eccezione degli elenchi di vettori
  • prepend per anteporre elementi ai tipi di elenchi, ad eccezione degli elenchi di vettori

Esegui eliminazioni

Naturalmente, puoi eliminare i dati dei film. I conservatori di film vorranno sicuramente che le pellicole fisiche vengano mantenute il più a lungo possibile.

# Delete by key
mutation DeleteMovie($id: UUID!) {
  movie_delete(id: $id)
}

Qui puoi utilizzare _deleteMany.

# Multiple deletes
mutation DeleteUnpopularMovies($minRating: Int!) {
  movie_deleteMany(where: { rating: { le: $minRating } })
}

Scrivere mutazioni sulle relazioni

Osserva come utilizzare la mutazione implicita _upsert in una relazione.

# Create or update a one to one relation
mutation MovieMetadataUpsert($movieId: UUID!, $director: String!) {
  movieMetadata_upsert(
    data: { movie: { id: $movieId }, director: $director }
  )
}

Progettare schemi per mutazioni efficienti

Data Connect fornisce due importanti funzionalità che ti consentono di scrivere mutazioni più efficienti e salvare operazioni di andata e ritorno.

Gli scalari chiave sono identificatori di oggetti concisi che Data Connect vengono assemblati automaticamente dai campi chiave negli schemi. Gli scalari chiave riguardano l'efficienza, in quanto ti consentono di trovare in una singola chiamata informazioni sull'identità e sulla struttura dei tuoi dati. Sono particolarmente utili quando vuoi eseguire azioni sequenziali sui nuovi record e hai bisogno di un identificatore univoco da passare alle operazioni imminenti, nonché quando vuoi accedere alle chiavi relazionali per eseguire operazioni aggiuntive più complesse.

Utilizzando i valori del server, puoi consentire al server di compilare dinamicamente i campi delle tabelle utilizzando valori archiviati o facilmente calcolabili in base a particolari espressioni CEL lato server nell'argomento expr. Ad esempio, puoi definire un campo con un timestamp applicato quando si accede al campo utilizzando l'ora memorizzata in una richiesta di operazione, updatedAt: Timestamp! @default(expr: "request.time").

Scrivere mutazioni avanzate: consentire a Data Connect di fornire valori utilizzando la sintassi field_expr

Come descritto in Scalari chiave e valori del server, puoi progettare lo schema in modo che il server compili i valori per i campi comuni come id e le date in risposta alle richieste del client.

Inoltre, puoi utilizzare i dati, ad esempio gli ID utente, inviati negli oggetti Data Connect request dalle app client.

Quando implementi le mutazioni, utilizza la sintassi field_expr per attivare aggiornamenti generati dal server o accedere ai dati dalle richieste. Ad esempio, per trasmettere l'autorizzazione uid memorizzata in una richiesta a un'operazione _upsert, trasmetti "auth.uid" nel campo userId_expr.

# Add a movie to the user's favorites list
mutation AddFavoritedMovie($movieId: UUID!) @auth(level: USER) {
  favorite_movie_upsert(data: { userId_expr: "auth.uid", movieId: $movieId })
}

# Remove a movie from the user's favorites list
mutation DeleteFavoritedMovie($movieId: UUID!) @auth(level: USER) {
  favorite_movie_delete(key: { userId_expr: "auth.uid", movieId: $movieId })
}

In alternativa, in una comune app di elenchi di cose da fare, quando crei un nuovo elenco, puoi passare id_expr per indicare al server di generare automaticamente un UUID per l'elenco.

mutation CreateTodoListWithFirstItem(
  $listName: String!
) @transaction {
  # Step 1
  todoList_insert(data: {
    id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
    name: $listName,
  })
}

Per ulteriori informazioni, consulta gli scalari _Expr nella documentazione di riferimento sugli scalari.

Scrivere mutazioni avanzate: operazioni in più passaggi

Esistono molte situazioni in cui potresti voler includere più campi di scrittura (come gli inserimenti) in una mutazione. Potresti anche voler leggere il database durante l'esecuzione di una mutazione per cercare e verificare i dati esistenti prima di eseguire, ad esempio, inserimenti o aggiornamenti. Queste opzioni consentono di risparmiare operazioni di andata e ritorno e quindi costi.

Data Connect consente di eseguire una logica in più passaggi nelle mutazioni supportando:

  • Più campi di scrittura

  • Più campi di lettura nelle mutazioni (utilizzando la parola chiave del campo query).

  • L'istruzione @transaction, che fornisce il supporto delle transazioni familiare dei database relazionali.

  • La direttiva @check, che ti consente di valutare i contenuti delle letture utilizzando le espressioni CEL e, in base ai risultati di questa valutazione:

    • Procedi con le creazioni, gli aggiornamenti e le eliminazioni definiti da una mutazione
    • Procedi per restituire i risultati di un campo di query
    • Utilizza i messaggi restituiti per eseguire la logica appropriata nel codice client
  • La direttiva @redact, che consente di omettere i risultati del campo di query dai risultati del protocollo di trasferimento.

  • Il binding CEL response, che archivia i risultati accumulati di tutte le mutazioni e le query eseguite in un'operazione complessa in più passaggi. Puoi accedere all'associazione response:

    • In @check direttive, tramite l'argomento expr:
    • Con i valori del server, utilizzando la sintassi field_expr

L'istruzione @transaction

Il supporto per le mutazioni in più passaggi include la gestione degli errori tramite le transazioni.

La direttiva @transaction impone che una mutazione, con un singolo campo di scrittura (ad esempio _insert o _update) o con più campi di scrittura, venga sempre eseguita in una transazione di database.

  • Le mutazioni senza @transaction eseguono ogni campo radice uno dopo l'altro in sequenza. L'operazione mostra gli errori come errori parziali dei campi, ma non gli effetti delle esecuzioni successive.

  • Le mutazioni con @transaction hanno la garanzia di riuscire o fallire completamente. Se uno dei campi all'interno della transazione ha esito negativo, l'intera transazione viene annullata.

Le direttive @check e @redact

L'istruzione @check verifica che i campi specificati siano presenti nei risultati della query. Per testare i valori dei campi viene utilizzata un'espressione Common Expression Language (CEL). Il comportamento predefinito dell'istruzione è di controllare e rifiutare i nodi il cui valore è null o [] (elenchi vuoti).

La direttiva @redact oscura una parte della risposta del client. I campi oscurati vengono comunque valutati per gli effetti collaterali (incluse le modifiche ai dati e @check) e i risultati sono comunque disponibili per i passaggi successivi nelle espressioni CEL.

Utilizza @check, @check(message:) e @redact

Un utilizzo importante di @check e @redact è la ricerca di dati correlati per decidere se determinate operazioni devono essere autorizzate, utilizzando la ricerca nella logica, ma nascondendola ai client. La query può restituire messaggi utili per la gestione corretta nel codice client.

A titolo illustrativo, il seguente campo di query verifica se un richiedente dispone di un ruolo "amministratore" appropriato per visualizzare gli utenti che possono modificare un film.

query GetMovieEditors($movieId: UUID!) @auth(level: USER) {
  moviePermission(key: { movieId: $movieId, userId_expr: "auth.uid" }) @redact {
    role @check(expr: "this == 'admin'", message: "You must be an admin to view all editors of a movie.")
  }
  moviePermissions(where: { movieId: { eq: $movieId }, role: { eq: "editor" } }) {
    user {
      id
      username
    }
  }
}

Per saperne di più sulle direttive @check e @redact nei controlli di autorizzazione, consulta la sezione relativa alla ricerca dei dati di autorizzazione.

Utilizzare @check per convalidare le chiavi

Alcuni campi di mutazione, come _update, potrebbero non funzionare se non esiste un record con una chiave specificata. Analogamente, le ricerche possono restituire null o un elenco vuoto. Questi non sono considerati errori e pertanto non attivano rollback.

Per evitare questo risultato, verifica se le chiavi possono essere trovate utilizzando l'istruzione @check.

# Delete by key, error if not found
mutation MustDeleteMovie($id: UUID!) @transaction {
  movie_delete(id: $id) @check(expr: "this != null", message: "Movie not found, therefore nothing is deleted")
}

Utilizza il binding response per concatenare le mutazioni in più passaggi

L'approccio di base per creare record correlati, ad esempio un nuovo Movie e una voce MovieMetadata associata, consiste nel:

  1. Chiama una mutazione _insert per Movie
  2. Memorizza la chiave restituita del filmato creato
  3. Quindi, chiama una seconda mutazione _insert per creare il record MovieMetadata.

Con Data Connect, invece, puoi gestire questo caso comune in un'unica operazione in più passaggi accedendo ai risultati del primo _insert nel secondo _insert.

Creare un'app di recensioni di film di successo richiede molto lavoro. Monitoriamo la nostra lista di cose da fare con un nuovo esempio.

Utilizzare response per impostare i campi con i valori del server

Nella seguente mutazione dell'elenco delle cose da fare:

  • Il binding response rappresenta l'oggetto di risposta parziale finora, che include tutti i campi di mutazione di primo livello prima di quello corrente.
  • I risultati dell'operazione todoList_insert iniziale, che restituisce il campo id (chiave), vengono accessibili in un secondo momento in response.todoList_insert.id per poter inserire immediatamente un nuovo elemento dell'elenco di cose da fare.
mutation CreateTodoListWithFirstItem(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1:
  todoList_insert(data: {
    id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
    name: $listName,
  })
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoList_insert.id" # <-- Grab the newly generated ID from the partial response so far.
    content: $itemContent,
  })
}

Utilizza response per convalidare i campi utilizzando @check

response è disponibile anche in @check(expr: "..."), quindi puoi utilizzarlo per creare una logica lato server ancora più complessa. In combinazione con i passaggi query { … } nelle mutazioni, puoi ottenere molto di più senza ulteriori round trip client-server.

Nell'esempio seguente, tieni presente che @check ha già accesso a response.query perché @check viene sempre eseguito dopo il passaggio a cui è collegato.

mutation CreateTodoInNamedList(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1: Look up List.id by its name
  query
  @check(expr: "response.query.todoLists.size() > 0", message: "No such TodoList with the name!")
  @check(expr: "response.query.todoLists.size() < 2", message: "Ambiguous listName!") {
    todoLists(where: { name: $listName }) {
      id
    }
  }
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoLists[0].id" # <-- Now we have the parent list ID to insert to
    content: $itemContent,
  })
}

Per saperne di più sul binding response, consulta la documentazione di riferimento CEL.

Comprendere le operazioni interrotte con @transaction e query @check

Le modifiche in più passaggi possono riscontrare errori:

  • Le operazioni del database potrebbero non riuscire.
  • La logica di @check della query potrebbe interrompere le operazioni.

Data Connect consiglia di utilizzare la direttiva @transaction con le mutazioni in più passaggi. Ciò si traduce in un database più coerente e in risultati di mutazione più facili da gestire nel codice client:

  • Al primo errore o @check non riuscito, l'operazione verrà interrotta, quindi non è necessario gestire l'esecuzione di campi successivi o la valutazione di CEL.
  • I rollback vengono eseguiti in risposta a errori del database o alla logica @check, per ottenere uno stato coerente del database.
  • Al codice client viene sempre restituito un errore di rollback.

Potrebbero esserci alcuni casi d'uso in cui scegli di non utilizzare @transaction: potresti optare per la coerenza finale se, ad esempio, hai bisogno di una velocità effettiva, una scalabilità o una disponibilità maggiori. Tuttavia, devi gestire il database e il codice client per consentire i risultati:

  • Se un campo non va a buon fine a causa di operazioni sul database, i campi successivi continueranno a essere eseguiti. Tuttavia, gli errori @check interrompono comunque l'intera operazione.
  • I rollback non vengono eseguiti, il che significa che il database si trova in uno stato misto con alcuni aggiornamenti riusciti e altri non riusciti.
  • Le operazioni con @check potrebbero dare risultati più incoerenti se la logica di @check utilizza i risultati di letture e/o scritture in un passaggio precedente.
  • Il risultato restituito al codice client conterrà un mix più complesso di risposte positive e negative da gestire.

Direttive per le mutazioni di Data Connect

Oltre alle direttive utilizzate per definire tipi e tabelle, Data Connect fornisce le direttive @auth, @check, @redact e @transaction per aumentare il comportamento delle operazioni.

Direttiva Applicabile a Descrizione
@auth Query e mutazioni Definisce il criterio di autorizzazione per una query o una mutazione. Consulta la guida all'autorizzazione e all'attestazione.
@check Campi query nelle operazioni in più passaggi Verifica che i campi specificati siano presenti nei risultati della query. Per testare i valori dei campi viene utilizzata un'espressione Common Expression Language (CEL). Vedi Operazioni in più passaggi.
@redact Query oscura una parte della risposta del cliente. Vedi Operazioni in più passaggi.
@transaction Mutazioni Impone l'esecuzione di una mutazione sempre in una transazione di database. Vedi Operazioni in più passaggi.

Passaggi successivi

Potrebbe interessarti: