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 d'un schéma, de requêtes et de mutations pour utiliser vos données.
Le guide de démarrage a présenté un schéma d'application d'avis sur les films pour PostgreSQL. Ce guide explique plus en détail comment concevoir des schémas Data Connect pour PostgreSQL.
Ce guide associe des requêtes et des mutations Data Connect à des exemples de schémas. Pourquoi parler des requêtes (et des mutations) dans un guide sur les schémas Data Connect ? Comme les autres plates-formes basées sur GraphQL, Firebase Data Connect est une plate-forme de développement axée sur les requêtes. En tant que développeur, vous devez donc réfléchir aux données dont vos clients ont besoin lors de la modélisation des données. Cela aura une grande influence sur le schéma de données que vous développerez pour votre projet.
Ce guide commence par un nouveau schéma pour les critiques de films, puis couvre les requêtes et les mutations dérivées de ce schéma, et fournit enfin une liste SQL équivalente au schéma Data Connect principal.
Schéma d'une application d'avis sur les films
Imaginez que vous souhaitiez créer un service permettant aux utilisateurs d'envoyer et de consulter des critiques de films.
Vous avez besoin d'un schéma initial pour une telle application. Vous allez ensuite étendre ce schéma pour créer des requêtes relationnelles complexes.
Tableau des films
Le schéma des films contient des directives de base, comme:
@table(name)
et@col(name)
pour personnaliser les noms de table et de colonne SQL. Si vous ne spécifiez pas de nom, Data Connect génère des noms en snake_case.@col(dataType)
pour personnaliser les types de colonnes SQL.@default
pour configurer les valeurs par défaut des colonnes SQL lors de l'insertion.
Pour en savoir plus, consultez les documents de référence sur @table
, @col
et @default
.
# Movies
type Movie @table(name: "movie", key: "id") {
id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()")
title: String!
releaseYear: Int
genre: String @col(dataType: "varchar(20)")
rating: Int
description: String
}
Clés scalaires et valeurs de serveur
Avant d'examiner plus en détail l'application d'avis sur les films, présentons les scalaires clés et les valeurs de serveur Data Connect.
Les scalaires de clé sont des identifiants d'objet concis que Data Connect assemble automatiquement à partir des champs de clé de vos schémas. Les scalaires de clé sont axés sur 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, ainsi que lorsque vous souhaitez accéder à des clés relationnelles pour effectuer des opérations plus complexes.
À l'aide de valeurs de serveur, vous pouvez laisser le serveur renseigner de manière dynamique les champs de vos tables à l'aide de valeurs stockées ou facilement calculables en fonction d'expressions CEL côté serveur particulières 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")
.
Table des métadonnées de film
Voyons maintenant comment suivre les réalisateurs de films et configurer une relation individuelle avec Movie
.
Ajoutez le champ de référence pour définir une relation.
Vous pouvez utiliser la directive @ref
pour personnaliser la contrainte de clé étrangère.
@ref(fields)
pour spécifier les champs de clé étrangère.@ref(references)
pour spécifier les champs référencés dans la table cible. Cette référence est définie par défaut sur la clé primaire, mais les champs avec@unique
sont également acceptés.
Pour en savoir plus, consultez la documentation de référence sur @ref
.
# Movie Metadata
# Movie - MovieMetadata is a one-to-one relationship
type MovieMetadata @table {
# @unique ensures that each Movie only has one MovieMetadata.
movie: Movie! @unique
# Since it references to another table type, it adds a foreign key constraint.
# movie: Movie! @unique @ref(fields: "movieId", references: "id")
# movieId: UUID! <- implicitly added foreign key field
director: String
}
Acteur et MovieActor
Vous souhaitez ensuite que des acteurs jouent dans vos films. Étant donné que vous avez une relation de plusieurs à plusieurs entre les films et les acteurs, créez une table de jointure.
# Actors
# Suppose an actor can participate in multiple movies and movies can have multiple actors
# Movie - Actors (or vice versa) is a many to many relationship
type Actor @table {
id: UUID! @default(expr: "uuidV4()")
name: String! @col(dataType: "varchar(30)")
}
# Join table for many-to-many relationship for movies and actors
# The 'key' param signifies the primary keys of this table
# In this case, the keys are [movieId, actorId], the foreign key fields of the reference fields [movie, actor]
type MovieActor @table(key: ["movie", "actor"]) {
movie: Movie!
# movieId: UUID! <- implicitly added foreign key field
actor: Actor!
# actorId: UUID! <- implicitly added foreign key field
role: String! # "main" or "supporting"
# optional other fields
}
Utilisateur
Enfin, les utilisateurs de votre application.
# Users
# Suppose a user can leave reviews for movies
type User @table {
id: String! @default(expr: "auth.uid")
username: String! @col(dataType: "varchar(50)")
}
Types de données acceptés
Data Connect est compatible avec les types de données scalaires suivants, avec des attributions à des types PostgreSQL à l'aide de @col(dataType:)
.
Type Data Connect | Type intégré GraphQL ou type personnalisé Data Connect |
Type PostgreSQL par défaut | Types PostgreSQL compatibles (alias entre parenthèses) |
---|---|---|---|
Chaîne | GraphQL | texte | text bit(n), varbit(n) char(n), varchar(n) |
Int | GraphQL | int | Int2 (smallint, smallserial), int4 (entier, int, serial) |
Float | GraphQL | float8 | float4 (réel) float8 (double précision) numérique (décimal) |
Booléen | GraphQL | booléen | booléen |
UUID | Personnalisé | uuid | uuid |
Int64 | Personnalisé | bigint | int8 (bigint, bigserial) numérique (décimal) |
Date | Personnalisé | date | date |
Horodatage | Personnalisé | timestamptz | timestamptz Remarque:Les informations sur le fuseau horaire local ne sont pas stockées. |
Vecteur | Personnalisé | vector | vecteur Consultez Effectuer une recherche de similarité vectorielle avec Vertex AI. |
List
GraphQL correspond à un tableau à une dimension.- Par exemple,
[Int]
correspond àint5[]
,[Any]
àjsonb[]
. - Data Connect n'est pas compatible avec les tableaux imbriqués.
- Par exemple,
Utiliser des champs générés pour créer des requêtes et des mutations
Vos requêtes et mutations Data Connect étendent un ensemble de champs Data Connect générés automatiquement en fonction des types et des relations de type de votre schéma. Ces champs sont générés par des outils locaux chaque fois que vous modifiez votre schéma.
Comme vous l'avez découvert dans le guide de démarrage, la console Firebase et nos outils de développement local utilisent ces champs générés automatiquement pour vous fournir des requêtes et des mutations ad hoc que vous pouvez utiliser pour insérer des données et vérifier le contenu de vos tables.
Dans votre processus de développement, vous allez implémenter des requêtes déployables et des mutations déployables groupées dans vos connecteurs, en fonction de ces champs générés automatiquement.
Nom des champs générés automatiquement
Data Connect infère des noms appropriés pour les champs générés automatiquement en fonction de vos déclarations de type de schéma. Par exemple, si vous travaillez avec une source PostgreSQL et que vous définissez une table nommée Movie
, le serveur génère:
- Champs pour la lecture des données dans les cas d'utilisation d'une seule table avec les noms explicites
movie
(au singulier, pour récupérer des résultats individuels en transmettant des arguments tels queeq
) etmovies
(au pluriel, pour récupérer des listes de résultats en transmettant des arguments tels quegt
et des opérations telles queorderby
). Data Connect génère également des champs pour les opérations relationnelles multitables avec des noms explicites tels queactors_on_movies
ouactors_via_actormovie
. - Champs permettant d'écrire des données avec un nom familier tel que
movie_insert
,movie_upsert
, etc.
Le langage de définition de schéma vous permet également de contrôler explicitement la manière dont les noms sont générés pour les champs à l'aide d'arguments de directive singular
et plural
.
Directives concernant les requêtes et les mutations
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 améliorer le comportement des requêtes et des mutations.
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 à 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 la section Opérations en plusieurs étapes. |
@redact |
Requêtes | Masque une partie de la réponse du client. Consultez la section Opérations en plusieurs étapes. |
@transaction |
Mutations | Exige qu'une mutation s'exécute toujours dans une transaction de base de données. Consultez la section Opérations en plusieurs étapes. |
Requêtes pour la base de données des critiques de films
Vous définissez une requête Data Connect avec une déclaration de type d'opération de requête, un nom d'opération, zéro ou plusieurs arguments d'opération, et zéro ou plusieurs directives avec des arguments.
Dans le guide de démarrage rapide, l'exemple de requête listEmails
ne comportait aucun paramètre. Bien entendu, dans de nombreux cas, les données transmises aux champs de requête seront dynamiques. Vous pouvez utiliser la syntaxe $variableName
pour utiliser des variables comme l'un des composants d'une définition de requête.
La requête suivante comporte donc:
- Définition de type
query
- Nom d'une opération (requête)
ListMoviesByGenre
- Un seul argument d'opération
$genre
à variable - Une seule directive,
@auth
.
query ListMoviesByGenre($genre: String!) @auth(level: USER)
Chaque argument de requête nécessite une déclaration de type, un élément intégré tel que String
ou un type personnalisé défini par le schéma tel que Movie
.
Examinons la signature de requêtes de plus en plus complexes. Vous terminerez par présenter des expressions de relation puissantes et concises que vous pouvez utiliser pour créer vos requêtes déployables.
Clés scalaires dans les requêtes
Mais d'abord, une remarque concernant les clés scalaires.
Data Connect définit un type spécial pour les scalaires de clé, identifiés par _Key
. Par exemple, le type d'un scalaire de clé pour notre table Movie
est Movie_Key
.
Vous récupérez les scalaires de clé en tant que réponse renvoyée par la plupart des champs de lecture générés automatiquement, ou bien sûr à partir de requêtes pour lesquelles vous avez récupéré tous les champs nécessaires à la création de la clé scalaire.
Les requêtes automatiques singulières, comme movie
dans notre exemple en cours, acceptent un argument de clé qui accepte un scalaire de clé.
Vous pouvez transmettre une clé scalaire en tant que littéral. Toutefois, vous pouvez définir des variables pour transmettre des scalaires de clé en entrée.
query GetMovie($myKey: Movie_Key!) {
movie(key: $myKey) { title }
}
Vous pouvez les fournir dans la requête JSON comme suit (ou dans d'autres formats de sérialisation):
{
# …
"variables": {
"myKey": {"foo": "some-string-value", "bar": 42}
}
}
Grâce à l'analyse scalaire personnalisée, un Movie_Key
peut également être construit à l'aide de la syntaxe d'objet, qui peut contenir des variables. Cette méthode est particulièrement utile lorsque vous souhaitez diviser des composants individuels en différentes variables pour une raison quelconque.
Aliasage dans les requêtes
Data Connect est compatible avec l'aliasing GraphQL dans les requêtes. Les alias vous permettent de renommer les données renvoyées dans les résultats d'une requête. Une seule requête Data Connect peut appliquer plusieurs filtres ou d'autres opérations de requête dans une seule requête efficace au serveur, en émettant effectivement plusieurs "sous-requêtes" à la fois. Pour éviter les conflits de noms dans l'ensemble de données renvoyé, vous devez utiliser des alias pour distinguer les sous-requêtes.
Voici une requête dans laquelle une expression utilise l'alias mostPopular
.
query ReviewTopPopularity($genre: String) {
mostPopular: review(first: {
where: {genre: {eq: $genre}},
orderBy: {popularity: DESC}
}) { … }
}
Requêtes simples avec filtres
Les requêtes Data Connect correspondent à tous les filtres et opérations d'ordre SQL courants.
Opérateurs where
et orderBy
(requêtes au singulier et au pluriel)
Renvoie toutes les lignes correspondantes de la table (et les associations imbriquées). Renvoie un tableau vide si aucun enregistrement ne correspond au filtre.
query MovieByTopRating($genre: String) {
mostPopular: movies(
where: { genre: { eq: $genre } }, orderBy: { rating: DESC }
) {
# graphql: list the fields from the results to return
id
title
genre
description
}
}
query MoviesByReleaseYear($min: Int, $max: Int) {
movies(where: {releaseYear: {le: $max, ge: $min}}, orderBy: [{releaseYear: ASC}]) { … }
}
Opérateurs limit
et offset
(requêtes au singulier et au pluriel)
Vous pouvez effectuer une pagination des résultats. Ces arguments sont acceptés, mais ne sont pas renvoyés dans les résultats.
query MoviesTop10 {
movies(orderBy: [{ rating: DESC }], limit: 10) {
# graphql: list the fields from the results to return
title
}
}
inclut pour les champs de tableau
Vous pouvez vérifier qu'un champ de tableau inclut un élément spécifié.
# Filter using arrays and embedded fields.
query ListMoviesByTag($tag: String!) {
movies(where: { tags: { includes: $tag }}) {
# graphql: list the fields from the results to return
id
title
}
}
Opérations sur les chaînes et expressions régulières
Vos requêtes peuvent utiliser des opérations de recherche et de comparaison de chaînes typiques, y compris des expressions régulières. Notez que, pour plus d'efficacité, vous regroupez plusieurs opérations ici et les clarifiez à l'aide d'alias.
query MoviesTitleSearch($prefix: String, $suffix: String, $contained: String, $regex: String) {
prefixed: movies(where: {title: {startsWith: $prefix}}) {...}
suffixed: movies(where: {title: {endsWith: $suffix}}) {...}
contained: movies(where: {title: {contains: $contained}}) {...}
matchRegex: movies(where: {title: {pattern: {regex: $regex}}}) {...}
}
or
et and
pour les filtres composés
Utilisez or
et and
pour une logique plus complexe.
query ListMoviesByGenreAndGenre($minRating: Int!, $genre: String) {
movies(
where: { _or: [{ rating: { ge: $minRating } }, { genre: { eq: $genre } }] }
) {
# graphql: list the fields from the results to return
title
}
}
Requêtes complexes
Les requêtes Data Connect peuvent accéder aux données en fonction des relations entre les tables. Vous pouvez utiliser les relations d'objet (un à un) ou de tableau (un à plusieurs) définies dans votre schéma pour effectuer des requêtes imbriquées, c'est-à-dire extraire les données d'un type avec celles d'un type imbriqué ou associé.
Ces requêtes utilisent la syntaxe magique Data Connect _on_
et _via
dans les champs de lecture générés.
Vous allez modifier le schéma à partir de notre version initiale.
Plusieurs à un
Ajoutons des avis à notre application, avec une table Review
et des modifications apportées à User
.
# User table is keyed by Firebase Auth UID.
type User @table {
# `@default(expr: "auth.uid")` sets it to Firebase Auth UID during insert and upsert.
id: String! @default(expr: "auth.uid")
username: String! @col(dataType: "varchar(50)")
# The `user: User!` field in the Review table generates the following one-to-many query field.
# reviews_on_user: [Review!]!
# The `Review` join table the following many-to-many query field.
# movies_via_Review: [Movie!]!
}
# Reviews is a join table tween User and Movie.
# It has a composite primary keys `userUid` and `movieId`.
# A user can leave reviews for many movies. A movie can have reviews from many users.
# User <-> Review is a one-to-many relationship
# Movie <-> Review is a one-to-many relationship
# Movie <-> User is a many-to-many relationship
type Review @table(name: "Reviews", key: ["movie", "user"]) {
user: User!
# The user field adds the following foreign key field. Feel free to uncomment and customize it.
# userUid: String!
movie: Movie!
# The movie field adds the following foreign key field. Feel free to uncomment and customize it.
# movieId: UUID!
rating: Int
reviewText: String
reviewDate: Date! @default(expr: "request.time")
}
Requête de plusieurs à un
Voyons maintenant une requête avec un alias pour illustrer la syntaxe _via_
.
query UserMoviePreferences($username: String!) @auth(level: USER) {
users(where: { username: { eq: $username } }) {
likedMovies: movies_via_Review(where: { rating: { ge: 4 } }) {
title
genre
}
dislikedMovies: movies_via_Review(where: { rating: { le: 2 } }) {
title
genre
}
}
}
En face à face
Vous pouvez voir le schéma. Ci-dessous, le schéma est modifié à des fins d'illustration.
# Movies
type Movie
@table(name: "Movies", singular: "movie", plural: "movies", key: ["id"]) {
id: UUID! @col(name: "movie_id") @default(expr: "uuidV4()")
title: String!
releaseYear: Int @col(name: "release_year")
genre: String
rating: Int @col(name: "rating")
description: String @col(name: "description")
tags: [String] @col(name: "tags")
}
# Movie Metadata
# Movie - MovieMetadata is a one-to-one relationship
type MovieMetadata
@table(
name: "MovieMetadata"
) {
# @ref creates a field in the current table (MovieMetadata) that holds the primary key of the referenced type
# In this case, @ref(fields: "id") is implied
movie: Movie! @ref
# movieId: UUID <- this is created by the above @ref
director: String @col(name: "director")
}
extend type MovieMetadata {
movieId: UUID! # matches primary key of referenced type
...
}
extend type Movie {
movieMetadata: MovieMetadata # can only be non-nullable on ref side
# conflict-free name, always generated
movieMetadatas_on_movie: MovieMetadata
}
Requête pour une session individuelle
Vous pouvez effectuer des requêtes à l'aide de la syntaxe _on_
.
# One to one
query GetMovieMetadata($id: UUID!) @auth(level: PUBLIC) {
movie(id: $id) {
movieMetadatas_on_movie {
director
}
}
}
Plusieurs à plusieurs
Les films ont besoin d'acteurs, et les acteurs ont besoin de films. Ils ont une relation de plusieurs à plusieurs que vous pouvez modéliser avec une table de jointure MovieActors
.
# MovieActors Join Table Definition
type MovieActors @table(
key: ["movie", "actor"] # join key triggers many-to-many generation
) {
movie: Movie!
actor: Actor!
}
# generated extensions for the MovieActors join table
extend type MovieActors {
movieId: UUID!
actorId: UUID!
}
# Extensions for Actor and Movie to handle many-to-many relationships
extend type Movie {
movieActors: [MovieActors!]! # standard many-to-one relation to join table
actors: [Actor!]! # many-to-many via join table
movieActors_on_actor: [MovieActors!]!
# since MovieActors joins distinct types, type name alone is sufficiently precise
actors_via_MovieActors: [Actor!]!
}
extend type Actor {
movieActors: [MovieActors!]! # standard many-to-one relation to join table
movies: [Movie!]! # many-to-many via join table
movieActors_on_movie: [MovieActors!]!
movies_via_MovieActors: [Movie!]!
}
Requête de type "plusieurs à plusieurs"
Examinons une requête avec un alias pour illustrer la syntaxe _via_
.
query GetMovieCast($movieId: UUID!, $actorId: UUID!) @auth(level: PUBLIC) {
movie(id: $movieId) {
mainActors: actors_via_MovieActor(where: { role: { eq: "main" } }) {
name
}
supportingActors: actors_via_MovieActor(
where: { role: { eq: "supporting" } }
) {
name
}
}
actor(id: $actorId) {
mainRoles: movies_via_MovieActor(where: { role: { eq: "main" } }) {
title
}
supportingRoles: movies_via_MovieActor(
where: { role: { eq: "supporting" } }
) {
title
}
}
}
Requêtes d'agrégation
Que sont les agrégations et pourquoi les utiliser ?
Les champs agrégatifs vous permettent d'effectuer des calculs sur une liste de résultats. Avec les champs agrégables, vous pouvez effectuer les opérations suivantes, par exemple:
- Trouver la note moyenne d'un avis
- Trouver le coût total des articles d'un panier
- Trouver le produit le mieux ou le moins bien noté
- Compter le nombre de produits dans votre magasin
Les agrégations sont effectuées sur le serveur, ce qui présente un certain nombre d'avantages par rapport au calcul côté client:
- Amélioration des performances de l'application (car vous évitez les calculs côté client)
- Réduction des coûts de sortie des données (puisque vous n'envoyez que les résultats agrégés au lieu de toutes les entrées)
- Sécurité améliorée (puisque vous pouvez accorder aux clients l'accès à des données agrégées au lieu de l'ensemble de données complet)
Exemple de schéma pour les agrégations
Dans cette section, nous allons passer à un exemple de schéma de vitrine, qui est utile pour expliquer comment utiliser les agrégations:
type Product @table {
name: String!
manufacturer: String!
quantityInStock: Int!
price: Float!
expirationDate: Date
}
Agrégations simples
_count pour tous les champs
Le champ d'agrégation le plus simple est _count
. Il indique le nombre de lignes correspondant à votre requête. Pour chaque champ de votre type, Data Connect génère les champs agrégés correspondants en fonction du type de champ.
Requête
query CountProducts {
products {
_count
}
}
Réponse one
one
Par exemple, si votre base de données contient cinq produits, le résultat sera le suivant:
{
"products": [
{
"_count": 5
}
]
}
Tous les champs comportent un champ <field>_count
, qui compte le nombre de lignes dont la valeur est non nulle dans ce champ.
Requête
query CountProductsWithExpirationDate {
products {
expirationDate_count
}
}
Réponsefield_count
field_count
Par exemple, si vous avez trois produits avec une date d'expiration, le résultat sera le suivant:
{
"products": [
{
"expirationDate_count": 3
}
]
}
_min, _max, _sum et _avg pour les champs numériques
Les champs numériques (int, float, int64) comportent également <field>_min
, <field>_max
, <field>_sum
et <field>_avg
.
Requête
query NumericAggregates {
products {
quantityInStock_max
price_min
price_avg
quantityInStock_sum
}
}
Réponse_min _max _sum _avg
_min _max _sum _avg
Par exemple, si vous disposez des produits suivants:
- Produit A:
quantityInStock: 10
,price: 2.99
- Produit B:
quantityInStock: 5
,price: 5.99
- Produit C:
quantityInStock: 20
,price: 1.99
Le résultat sera:
{
"products": [
{
"quantityInStock_max": 20,
"price_min": 1.99,
"price_avg": 3.6566666666666666,
"quantityInStock_sum": 35
}
]
}
_min et _max pour les dates et les codes temporels
Les champs de date et d'horodatage contiennent <field>_min
et <field>_max
.
Requête
query DateAndTimeAggregates {
products {
expirationDate_max
expirationDate_min
}
}
Réponse_min _maxdatetime
_min _maxdatetime
Par exemple, si vous disposez des dates d'expiration suivantes:
- Produit A:
2024-01-01
- Produit B:
2024-03-01
- Produit C:
2024-02-01
Le résultat sera:
{
"products": [
{
"expirationDate_max": "2024-03-01",
"expirationDate_min": "2024-01-01"
}
]
}
Distinct
L'argument distinct
vous permet d'obtenir toutes les valeurs uniques d'un champ (ou d'une combinaison de champs). Exemple :
Requête
query ListDistinctManufacturers {
products(distinct: true) {
manufacturer
}
}
Réponsedistinct
distinct
Par exemple, si vous disposez des fabricants suivants:
- Produit A:
manufacturer: "Acme"
- Produit B:
manufacturer: "Beta"
- Produit C:
manufacturer: "Acme"
Le résultat sera:
{
"products": [
{ "manufacturer": "Acme" },
{ "manufacturer": "Beta" }
]
}
Vous pouvez également utiliser l'argument distinct
sur les champs agrégables pour agréger les valeurs distinctes. Exemple :
Requête
query CountDistinctManufacturers {
products {
manufacturer_count(distinct: true)
}
}
Réponsedistinctonaggregate
distinctonaggregate
Par exemple, si vous disposez des fabricants suivants:
- Produit A:
manufacturer: "Acme"
- Produit B:
manufacturer: "Beta"
- Produit C:
manufacturer: "Acme"
Le résultat sera:
{
"products": [
{
"manufacturer_count": 2
}
]
}
Agrégations groupées
Pour effectuer une agrégation groupée, sélectionnez un ensemble de champs d'agrégation et de non-agrégation sur un type. Cela regroupe toutes les lignes correspondantes ayant la même valeur pour les champs non agrégés, puis calcule les champs agrégés pour ce groupe. Exemple :
Requête
query MostExpensiveProductByManufacturer {
products {
manufacturer
price_max
}
}
Réponsegroupedaggregates
groupedaggregates
Par exemple, si vous disposez des produits suivants:
- Produit A:
manufacturer: "Acme"
,price: 2.99
- Produit B:
manufacturer: "Beta"
,price: 5.99
- Produit C:
manufacturer: "Acme"
,price: 1.99
Le résultat est le suivant:
{
"products": [
{ "manufacturer": "Acme", "price_max": 2.99 },
{ "manufacturer": "Beta", "price_max": 5.99 }
]
}
having
et where
avec des agrégations groupées
Vous pouvez également utiliser les arguments having
et where
pour ne renvoyer que les groupes qui répondent à des critères fournis.
having
vous permet de filtrer les groupes en fonction de leurs champs agrégés.where
vous permet de filtrer les lignes en fonction de champs non agrégés.
Requête
query FilteredMostExpensiveProductByManufacturer {
products(having: {price_max: {ge: 2.99}}) {
manufacturer
price_max
}
}
Réponsehavingwhere
havingwhere
Par exemple, si vous disposez des produits suivants:
- Produit A:
manufacturer: "Acme"
,price: 2.99
- Produit B:
manufacturer: "Beta"
,price: 5.99
- Produit C:
manufacturer: "Acme"
,price: 1.99
Le résultat sera:
{
"products": [
{ "manufacturer": "Acme", "price_max": 2.99 },
{ "manufacturer": "Beta", "price_max": 5.99 }
]
}
Agrégation entre les tables
Les champs agrégables peuvent être utilisés conjointement avec les champs de relation de type "un à plusieurs" générés pour répondre à des questions complexes sur vos données. Voici un schéma modifié, avec une table distincte, Manufacturer
, que nous pouvons utiliser dans des exemples:
type Product @table {
name: String!
manufacturer: Manufacturer!
quantityInStock: Int!
price: Float!
expirationDate: Date
}
type Manufacturer @table {
name: String!
headquartersCountry: String!
}
Nous pouvons désormais utiliser des champs agrégés pour déterminer, par exemple, le nombre de produits fabriqués par un fabricant:
Requête
query GetProductCount($id: UUID) {
manufacturers {
name
products_on_manufacturer {
_count
}
}
}
Réponse aggregatesacrosstables
aggregatesacrosstables
Par exemple, si vous disposez des fabricants suivants:
- Fabricant A:
name: "Acme"
,products_on_manufacturer: 2
- Fabricant B:
name: "Beta"
,products_on_manufacturer: 1
Le résultat sera:
{
"manufacturers": [
{ "name": "Acme", "products_on_manufacturer": { "_count": 2 } },
{ "name": "Beta", "products_on_manufacturer": { "_count": 1 } }
]
}
Mutations pour la base de données des critiques de films
Comme indiqué, lorsque vous définissez une table dans votre schéma, Data Connect génère des champs _insert
, _update
, etc. de base pour chaque table.
type Movie @table { ... }
extend type Mutation {
# Insert a row into the movie table.
movie_insert(...): Movie_Key!
# Upsert a row into movie."
movie_upsert(...): Movie_Key!
# Update a row in Movie. Returns null if a row with the specified id/key does not exist
movie_update(...): Movie_Key
# Update rows based on a filter in Movie.
movie_updateMany(...): Int!
# Delete a single row in Movie. Returns null if a row with the specified id/key does not exist
movie_delete(...): Movie_Key
# Delete rows based on a filter in Movie.
movie_deleteMany(...): Int!
}
Vous pouvez ainsi implémenter des cas CRUD de base de plus en plus complexes. Dites ça rapidement cinq fois de suite !
Créer
Créons des éléments 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 mise à jour.
# 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 informations à jour. Les producteurs et les réalisateurs espèrent certainement que ces notes moyennes sont à la hausse.
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 diminution, d'ajout et de préfixage avec _update
Bien que vous puissiez définir explicitement des valeurs dans data:
pour les mutations _update
et _updateMany
, il est souvent plus logique d'appliquer un opérateur tel qu'un incrément pour mettre à jour les valeurs.
Pour modifier l'exemple de mise à jour précédent, supposons que vous souhaitiez augmenter la note d'un film 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 champ:
inc
pour incrémenter les types de donnéesInt
,Int64
,Float
,Date
etTimestamp
dec
pour diminuer les types de donnéesInt
,Int64
,Float
,Date
etTimestamp
Pour les listes, vous pouvez également mettre à jour des valeurs individuelles ou des listes de valeurs à l'aide des éléments suivants:
add
pour ajouter un ou plusieurs éléments s'ils ne sont pas déjà présents dans les types de listes, à l'exception des listes Vectorremove
pour supprimer tous les éléments, le cas échéant, des types de listes, à l'exception des listes Vectorappend
pour ajouter un ou plusieurs éléments à des types de listes, à l'exception des listes Vectorprepend
pour ajouter un ou plusieurs éléments au début des types de listes, à l'exception des listes vectorielles
Effectuer des suppressions
Vous pouvez bien sûr supprimer les données de film. Les conservateurs de films souhaitent 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 des 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 }
)
}
Laisser Data Connect fournir des valeurs à l'aide de la syntaxe field_expr
Comme indiqué dans la section Valeurs scalaires clés et valeurs du 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 client.
De plus, vous pouvez utiliser des données, telles que des ID utilisateur, envoyées dans des objets request
Data Connect à partir d'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.
Opérations en plusieurs étapes
Dans de nombreux cas, vous pouvez souhaiter 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:
Plusieurs champs d'écriture
Plusieurs champs de lecture dans vos mutations (à l'aide du mot clé de champ
query
).La directive
@transaction
, qui fournit une compatibilité avec les transactions familière des bases de données relationnelles.La directive
@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, mises à jour et suppressions définies par une mutation
- Afficher les résultats d'un champ de requête
- Utiliser les messages renvoyés pour exécuter la logique appropriée dans votre code client
La directive
@redact
, qui vous permet d'omettre les résultats du champ de requête des résultats du protocole filaire.La liaison
response
CEL, 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 liaisonresponse
:- Dans les directives
@check
, via l'argumentexpr:
- Avec des valeurs de serveur, à l'aide de la syntaxe
field_expr
- Dans les directives
Directive @transaction
La prise en charge des mutations multi-étapes inclut la gestion des erreurs à l'aide de transactions.
La directive @transaction
applique le fait 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 toutes les erreurs sous forme d'erreurs de champ partielles, mais pas les conséquences des exécutions ultérieures.Les mutations avec
@transaction
aboutissent ou échouent 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) permet de 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 détecter les effets secondaires (y compris les modifications de données et @check
), et les résultats sont toujours disponibles pour les étapes ultérieures des expressions CEL.
Utiliser @check
, @check(message:)
et @redact
L'une des principales utilisations de l'@redact
d'annonce @check
consiste à rechercher des données associées pour déterminer si certaines opérations doivent être autorisées, en utilisant la recherche dans la logique, mais en la cachant aux clients. Votre requête peut renvoyer des messages utiles pour une gestion correcte dans le code client.
À titre d'illustration, le champ de requête suivant vérifie si un demandeur dispose d'un rôle "administrateur" approprié pour afficher les utilisateurs autorisés à 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 des clés
Certains champs de mutation, tels que _update
, peuvent ne pas être exécutés 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 rollbacks.
Pour éviter ce résultat, vérifiez 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 nouvel Movie
et une entrée MovieMetadata
associée, consiste à:
- Appeler une mutation
_insert
pourMovie
- Stocker la clé renvoyée du film créé
- Appelez ensuite une deuxième mutation
_insert
pour créer l'enregistrementMovieMetadata
.
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 demande beaucoup de travail. Suivons notre liste de tâches avec 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 partiel jusqu'à présent, qui inclut tous les champs de mutation de niveau supérieur avant celui en cours. - Les résultats de l'opération
todoList_insert
initiale, qui renvoie le champid
(clé), sont accessibles plus tard dansresponse.todoList_insert.id
afin que nous puissions insérer immédiatement un nouvel élément de la 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 des 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 CEL.
Comprendre les opérations interrompues avec @transaction
et query @check
Les mutations en plusieurs étapes peuvent rencontrer des erreurs:
- Les opérations de base de données peuvent échouer.
- La logique
@check
de la requête peut mettre fin aux opérations.
Data Connect vous recommande d'utiliser la directive @transaction
avec vos mutations en plusieurs étapes. Vous obtenez ainsi une base de données plus cohérente et des résultats de mutation plus faciles à gérer dans le code client:
- À la première erreur ou à l'échec de
@check
, l'opération s'arrête. Il n'est donc pas nécessaire de gérer l'exécution des champs ou l'évaluation de CEL ultérieurs. - 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 de base de données cohérent. - Une erreur de rollback est toujours renvoyée au code client.
Dans certains cas d'utilisation, vous pouvez choisir de ne pas utiliser @transaction
. Vous pouvez opter pour une cohérence à terme si, par exemple, 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 permettre les résultats:
- Si un champ échoue en raison d'opérations de base de données, les champs suivants continuent de s'exécuter. Toutefois, les
@check
qui échouent mettent toujours fin à l'ensemble de l'opération. - Les rollbacks ne sont pas effectués, ce qui signifie que l'état de la base de données est mixte, avec certaines mises à jour réussies et d'autres échouées.
- Vos opérations avec
@check
peuvent donner des résultats plus incohérents si votre logique@check
utilise les résultats des lectures et/ou des écritures à 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 à gérer.
Schéma SQL équivalent
-- Movies Table
CREATE TABLE Movies (
movie_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
title VARCHAR(255) NOT NULL,
release_year INT,
genre VARCHAR(30),
rating INT,
description TEXT,
tags TEXT[]
);
-- Movie Metadata Table
CREATE TABLE MovieMetadata (
movie_id UUID REFERENCES Movies(movie_id) UNIQUE,
director VARCHAR(255) NOT NULL,
PRIMARY KEY (movie_id)
);
-- Actors Table
CREATE TABLE Actors (
actor_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
name VARCHAR(30) NOT NULL
);
-- MovieActor Join Table for Many-to-Many Relationship
CREATE TABLE MovieActor (
movie_id UUID REFERENCES Movies(movie_id),
actor_id UUID REFERENCES Actors(actor_id),
role VARCHAR(50) NOT NULL, # "main" or "supporting"
PRIMARY KEY (movie_id, actor_id),
FOREIGN KEY (movie_id) REFERENCES Movies(movie_id),
FOREIGN KEY (actor_id) REFERENCES Actors(actor_id)
);
-- Users Table
CREATE TABLE Users (
user_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_auth VARCHAR(255) NOT NULL
username VARCHAR(30) NOT NULL
);
-- Reviews Table
CREATE TABLE Reviews (
review_id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
user_id UUID REFERENCES Users(user_id),
movie_id UUID REFERENCES Movies(movie_id),
rating INT,
review_text TEXT,
review_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE (movie_id, user_id)
FOREIGN KEY (user_id) REFERENCES Users(user_id),
FOREIGN KEY (movie_id) REFERENCES Movies(movie_id)
);
-- Self Join Example for Movie Sequel Relationship
ALTER TABLE Movies
ADD COLUMN sequel_to UUID REFERENCES Movies(movie_id);
Étape suivante
- Découvrez comment sécuriser vos requêtes et vos mutations à l'aide de l'autorisation et de l'attestation.
- Découvrez comment appeler vos requêtes et vos mutations à partir d'un SDK Web, d'un SDK Android, d'un SDK iOS et d'un SDK Flutter générés automatiquement.