Implémenter des mutations Data Connect

Firebase Data Connect vous permet de créer des connecteurs pour vos instances PostgreSQL gérées avec Google Cloud SQL. Ces connecteurs sont des combinaisons de requêtes et de mutations permettant d'utiliser les données de votre schéma.

Le guide de démarrage a présenté un schéma d'application d'avis sur des films pour PostgreSQL.

Ce guide présentait également les opérations administratives déployables et ponctuelles, y compris les mutations.

  • Les mutations déployables sont celles que vous implémentez pour appeler à partir d'applications clientes dans un connecteur, avec les points de terminaison d'API que vous définissez. Data Connect intègre l'authentification et l'autorisation dans ces mutations, et génère des SDK clients en fonction de votre API.
  • Les mutations administratives ponctuelles sont exécutées à partir d'environnements privilégiés pour remplir et gérer les tables. Vous pouvez les créer et les exécuter dans la console Firebase, à partir d'environnements privilégiés à l'aide de Firebase Admin SDK, et dans des environnements de développement locaux à l'aide de notre extension VS Code Data Connect.

Ce guide examine plus en détail les mutations déployables.

Fonctionnalités des mutations Data Connect

Data Connect vous permet d'effectuer des mutations de base de toutes les manières possibles avec une base de données PostgreSQL :

  • Effectuer des opérations CRUD
  • Gérer les opérations en plusieurs étapes avec des transactions

Toutefois, grâce aux extensions de Data Connect pour GraphQL, vous pouvez implémenter des mutations avancées pour des applications plus rapides et plus efficaces :

  • Utilisez les scalaires de clé renvoyés par de nombreuses opérations pour simplifier les opérations répétées sur les enregistrements.
  • Utilisez les valeurs de serveur pour remplir les données avec les opérations fournies par le serveur.
  • Exécutez des requêtes au cours d'opérations de mutation en plusieurs étapes pour rechercher des données, ce qui vous permet d'économiser des lignes de code et des allers-retours vers le serveur.

Utiliser des champs générés pour implémenter des mutations

Vos opérations Data Connect étendront un ensemble de champs Data Connect générés automatiquement en fonction des types et des relations de type dans votre schéma. Ces champs sont générés par des outils locaux chaque fois que vous modifiez votre schéma.

Vous pouvez utiliser des champs générés pour implémenter des mutations, de la création, la mise à jour et la suppression d'enregistrements individuels dans des tables uniques aux mises à jour multitables plus complexes.

Supposons que votre schéma contienne un type Movie et un type Actor associé. Data Connect génère les champs movie_insert, movie_update, movie_delete et plus encore.

Mutation avec le champ
movie_insert

Le champ movie_insert représente une mutation permettant de créer un seul enregistrement dans la table Movie.

Utilisez ce champ pour créer un film.

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

Mutation avec le champ
movie_update

Le champ movie_update représente une mutation permettant de mettre à jour un seul enregistrement dans la table Movie.

Utilisez ce champ pour mettre à jour un seul film à l'aide de sa clé.

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

Mutation avec le champ
movie_delete

Le champ movie_delete représente une mutation permettant de supprimer un seul enregistrement dans la table Movie.

Utilisez ce champ pour supprimer un film à l'aide de sa clé.

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

Éléments essentiels d'une mutation

Les mutations Data Connect sont des mutations GraphQL avec des extensions Data Connect. Comme pour une mutation GraphQL classique, vous pouvez définir un nom d'opération et une liste de variables GraphQL.

Data Connect étend les requêtes GraphQL avec des directives personnalisées telles que @auth et @transaction.

La mutation suivante comporte :

  • Définition du type mutation
  • Nom d'une opération SignUp (mutation)
  • Argument d'opération $username à une seule variable
  • Une seule directive, @auth
  • Un seul champ user_insert.
mutation SignUp($username: String!) @auth(level: USER) {
  user_insert(data: {
    id_expr: "auth.uid"
    username: $username
  })
}

Chaque argument de mutation nécessite une déclaration de type, un type intégré tel que String ou un type personnalisé défini par le schéma tel que Movie.

Écrire des mutations de base

Vous pouvez commencer à écrire des mutations pour créer, mettre à jour et supprimer des enregistrements individuels de votre base de données.

Créer

Effectuons des créations de 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
  })
}

Ou une opération d'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"
  })
}

Effectuer des mises à jour

Voici les dernières informations. Les producteurs et les réalisateurs espèrent certainement que ces notes moyennes sont en phase avec les tendances.

Le champ movie_update contient un argument id attendu pour identifier un enregistrement et un champ data que vous pouvez utiliser pour définir des valeurs dans cette mise à jour.

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

Pour effectuer plusieurs mises à jour, utilisez le champ 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
      })
}

Utiliser les opérations d'incrémentation, de décrémentation, d'ajout et de préfixe avec _update

Bien que vous puissiez définir explicitement des valeurs dans data: lors des mutations _update et _updateMany, il est souvent plus judicieux d'appliquer un opérateur tel que "increment" pour mettre à jour les valeurs.

Pour modifier l'exemple de mise à jour précédent, supposons que vous souhaitiez incrémenter la note d'un film en particulier. Vous pouvez utiliser la syntaxe rating_update avec l'opérateur inc.

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

Data Connect accepte les opérateurs suivants pour les mises à jour de champs :

  • inc pour incrémenter les types de données Int, Int64, Float, Date et Timestamp
  • dec pour décrémenter les types de données Int, Int64, Float, Date et Timestamp

Pour les listes, vous pouvez également effectuer des mises à jour avec des valeurs individuelles ou des listes de valeurs à l'aide des éléments suivants :

  • add pour ajouter des éléments s'ils ne sont pas déjà présents dans les types de listes, à l'exception des listes de vecteurs
  • remove pour supprimer tous les éléments des types de listes, le cas échéant, à l'exception des listes de vecteurs
  • append pour ajouter des éléments aux types de listes, à l'exception des listes de vecteurs
  • prepend pour ajouter un ou plusieurs éléments au début des types de listes, à l'exception des listes de vecteurs

Effectuer des suppressions

Vous pouvez bien sûr supprimer les données de films. Les spécialistes de la conservation des films souhaiteront certainement que les films physiques soient conservés le plus longtemps possible.

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

Vous pouvez utiliser _deleteMany.

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

Écrire des mutations sur les relations

Découvrez comment utiliser la mutation _upsert implicite sur une relation.

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

Concevoir des schémas pour des mutations efficaces

Data Connect fournit deux fonctionnalités importantes qui vous permettent d'écrire des mutations plus efficaces et d'économiser des opérations aller-retour.

Les scalaires clés sont des identifiants d'objet concis que Data Connect assemble automatiquement à partir des champs clés de vos schémas. Les scalaires clés concernent l'efficacité. Ils vous permettent de trouver en un seul appel des informations sur l'identité et la structure de vos données. Ils sont particulièrement utiles lorsque vous souhaitez effectuer des actions séquentielles sur de nouveaux enregistrements et que vous avez besoin d'un identifiant unique à transmettre aux opérations à venir. Ils le sont également lorsque vous souhaitez accéder à des clés relationnelles pour effectuer des opérations supplémentaires plus complexes.

En utilisant les valeurs de serveur, vous pouvez laisser le serveur remplir dynamiquement les champs de vos tableaux à l'aide de valeurs stockées ou facilement calculables, selon des expressions CEL côté serveur spécifiques dans l'argument expr. Par exemple, vous pouvez définir un champ avec un code temporel appliqué lorsque le champ est consulté à l'aide de l'heure stockée dans une requête d'opération, updatedAt: Timestamp! @default(expr: "request.time").

Écrire des mutations avancées : laisser Data Connect fournir des valeurs à l'aide de la syntaxe field_expr

Comme indiqué dans Scalaires clés et valeurs de serveur, vous pouvez concevoir votre schéma de sorte que le serveur renseigne les valeurs des champs courants tels que les id et les dates en réponse aux requêtes des clients.

De plus, vous pouvez utiliser des données telles que les ID utilisateur envoyés dans les objets Data Connect request des applications clientes.

Lorsque vous implémentez des mutations, utilisez la syntaxe field_expr pour déclencher des mises à jour générées par le serveur ou accéder aux données des requêtes. Par exemple, pour transmettre l'autorisation uid stockée dans une requête à une opération _upsert, transmettez "auth.uid" dans le champ 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 })
}

Dans une application de liste de tâches familière, lorsque vous créez une liste de tâches, vous pouvez transmettre id_expr pour demander au serveur de générer automatiquement un UUID pour la liste.

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,
  })
}

Pour en savoir plus, consultez les scalaires _Expr dans la documentation de référence sur les scalaires.

Écrire des mutations avancées : opérations en plusieurs étapes

Il existe de nombreuses situations dans lesquelles vous pouvez inclure plusieurs champs d'écriture (comme des insertions) dans une même mutation. Vous pouvez également lire votre base de données lors de l'exécution d'une mutation pour rechercher et valider les données existantes avant d'effectuer, par exemple, des insertions ou des mises à jour. Ces options permettent d'économiser des opérations aller-retour et donc des coûts.

Data Connect vous permet d'effectuer une logique en plusieurs étapes dans vos mutations en prenant en charge les éléments suivants :

  • Champs d'écriture multiples

  • Plusieurs champs de lecture dans vos mutations (à l'aide du mot clé de champ query).

  • La directive @transaction, qui fournit une assistance pour les transactions similaire à celle des bases de données relationnelles.

  • L'instruction @check, qui vous permet d'évaluer le contenu des lectures à l'aide d'expressions CEL et, en fonction des résultats de cette évaluation :

    • Effectuer les créations, les mises à jour et les suppressions définies par une mutation
    • Poursuivez pour renvoyer les résultats d'un champ de requête.
    • Utilisez les messages renvoyés pour effectuer la logique appropriée dans votre code client.
  • La directive @redact, qui vous permet d'omettre les résultats des champs de requête dans les résultats du protocole filaire.

  • L'association CEL response, qui stocke les résultats cumulés de toutes les mutations et requêtes effectuées dans une opération complexe en plusieurs étapes. Vous pouvez accéder à la liaison response :

    • Dans les directives @check, via l'argument expr:
    • Avec des valeurs de serveur, en utilisant la syntaxe field_expr

Directive @transaction

La prise en charge des mutations en plusieurs étapes inclut la gestion des erreurs à l'aide de transactions.

La directive @transaction impose qu'une mutation (avec un seul champ d'écriture, par exemple _insert ou _update, ou avec plusieurs champs d'écriture) s'exécute toujours dans une transaction de base de données.

  • Les mutations sans @transaction exécutent chaque champ racine l'un après l'autre, dans l'ordre. L'opération affiche les erreurs sous forme d'erreurs de champ partielles, mais pas les impacts des exécutions ultérieures.

  • Les mutations avec @transaction sont garanties de réussir ou d'échouer complètement. Si l'un des champs de la transaction échoue, l'intégralité de la transaction est annulée.

Directives @check et @redact

La directive @check vérifie que les champs spécifiés sont présents dans les résultats de la requête. Une expression CEL (Common Expression Language) est utilisée pour tester les valeurs des champs. Le comportement par défaut de la directive consiste à rechercher et à rejeter les nœuds dont la valeur est null ou [] (listes vides).

La directive @redact masque une partie de la réponse du client. Les champs masqués sont toujours évalués pour les effets secondaires (y compris les modifications de données et @check), et les résultats restent disponibles pour les étapes ultérieures dans les expressions CEL.

Utiliser @check, @check(message:) et @redact

@check et @redact sont principalement utilisés pour rechercher des données associées afin de déterminer si certaines opérations doivent être autorisées, en utilisant la recherche dans la logique, mais en la masquant aux clients. Votre requête peut renvoyer des messages utiles pour une gestion correcte dans le code client.

Pour illustrer cela, le champ de requête suivant vérifie si un demandeur dispose d'un rôle d'administrateur approprié pour afficher les utilisateurs qui peuvent modifier 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
    }
  }
}

Pour en savoir plus sur les directives @check et @redact dans les vérifications d'autorisation, consultez la discussion sur la recherche de données d'autorisation.

Utiliser @check pour valider les clés

Certains champs de mutation, tels que _update, peuvent ne rien faire si un enregistrement avec une clé spécifiée n'existe pas. De même, les recherches peuvent renvoyer une valeur nulle ou une liste vide. Elles ne sont pas considérées comme des erreurs et ne déclenchent donc pas de rétablissement.

Pour éviter ce résultat, testez si des clés peuvent être trouvées à l'aide de la directive @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")
}

Utiliser la liaison response pour enchaîner des mutations en plusieurs étapes

L'approche de base pour créer des enregistrements associés, par exemple un Movie et une entrée MovieMetadata associée, consiste à :

  1. Appeler une mutation _insert pour Movie
  2. Stocker la clé renvoyée du film créé
  3. Appelez ensuite une deuxième mutation _insert pour créer l'enregistrement MovieMetadata.

Toutefois, avec Data Connect, vous pouvez gérer ce cas courant en une seule opération en plusieurs étapes en accédant aux résultats du premier _insert dans le deuxième _insert.

Créer une application d'avis sur les films qui fonctionne bien demande beaucoup de travail. Suivons notre liste de tâches à l'aide d'un nouvel exemple.

Utiliser response pour définir des champs avec des valeurs de serveur

Dans la mutation de la liste de tâches suivante :

  • La liaison response représente l'objet de réponse partielle jusqu'à présent, qui inclut tous les champs de mutation de premier niveau avant celui en cours.
  • Les résultats de l'opération todoList_insert initiale, qui renvoie le champ id (clé), sont accessibles ultérieurement dans response.todoList_insert.id afin que nous puissions insérer immédiatement un nouvel élément de liste de tâches.
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,
  })
}

Utiliser response pour valider les champs à l'aide de @check

response est également disponible dans @check(expr: "..."). Vous pouvez donc l'utiliser pour créer une logique côté serveur encore plus complexe. Combiné aux étapes query { … } dans les mutations, vous pouvez accomplir beaucoup plus sans aucun aller-retour client-serveur supplémentaire.

Dans l'exemple suivant, notez que @check a déjà accès à response.query, car un @check s'exécute toujours après l'étape à laquelle il est associé.

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,
  })
}

Pour en savoir plus sur la liaison response, consultez la documentation de référence sur CEL.

Comprendre les opérations interrompues avec @transaction et query @check

Des erreurs peuvent se produire lors des mutations en plusieurs étapes :

  • Les opérations sur la base de données peuvent échouer.
  • La logique de la requête @check peut mettre fin aux opérations.

Data Connect vous recommande d'utiliser la directive @transaction avec vos mutations en plusieurs étapes. Cela permet d'obtenir une base de données plus cohérente et des résultats de mutation plus faciles à gérer dans le code client :

  • L'opération s'arrête à la première erreur ou au premier @check ayant échoué. Il n'est donc pas nécessaire de gérer l'exécution des champs suivants ni l'évaluation de CEL.
  • Les rollbacks sont effectués en réponse à des erreurs de base de données ou à une logique @check, ce qui permet d'obtenir un état cohérent de la base de données.
  • Une erreur de rétablissement est toujours renvoyée au code client.

Dans certains cas d'utilisation, vous pouvez choisir de ne pas utiliser @transaction. Par exemple, vous pouvez opter pour la cohérence à terme si vous avez besoin d'un débit, d'une évolutivité ou d'une disponibilité plus élevés. Toutefois, vous devez gérer votre base de données et votre code client pour autoriser les résultats :

  • Si un champ échoue en raison d'opérations de base de données, les champs suivants continueront de s'exécuter. Toutefois, les @check ayant échoué mettent toujours fin à l'opération dans son ensemble.
  • Les rollbacks ne sont pas effectués, ce qui signifie que l'état de la base de données est mixte (certaines mises à jour ont réussi et d'autres ont échoué).
  • Vos opérations avec @check peuvent donner des résultats plus incohérents si votre logique @check utilise les résultats de lectures et/ou d'écritures d'une étape précédente.
  • Le résultat renvoyé au code client contiendra un mélange plus complexe de réponses de réussite et d'échec à traiter.

Directives pour les mutations Data Connect

En plus des directives que vous utilisez pour définir des types et des tables, Data Connect fournit les directives @auth, @check, @redact et @transaction pour augmenter le comportement des opérations.

Directive Applicable à Description
@auth Requêtes et mutations Définit la règle d'autorisation pour une requête ou une mutation. Consultez le guide sur l'autorisation et l'attestation.
@check Champs query dans les opérations en plusieurs étapes Vérifie que les champs spécifiés sont présents dans les résultats de la requête. Une expression CEL (Common Expression Language) est utilisée pour tester les valeurs des champs. Consultez Opérations en plusieurs étapes.
@redact Requêtes Masque une partie de la réponse du client. Consultez Opérations en plusieurs étapes.
@transaction Mutations Garantit qu'une mutation s'exécute toujours dans une transaction de base de données. Consultez Opérations en plusieurs étapes.

Étapes suivantes

Vous pourriez être intéressé par :