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 schéma, de requêtes et de mutations pour utiliser vos données.
Le guide de démarrage présentait un schéma d'application de messagerie pour PostgreSQL, mais ce guide examine plus en détail comment concevoir des schémas pour PostgreSQL, en utilisant désormais une base de données d'avis sur des films comme exemple motivant.Data Connect
Ce guide associe des requêtes et des mutations Data Connect à des exemples de schémas. Pourquoi aborder les requêtes (et les mutations) dans un guide sur les Data Connect schémas ? Comme d'autres plates-formes basées sur GraphQL, Firebase Data Connect est une plate-forme de développement axée sur les requêtes. Par conséquent, en tant que développeur, vous penserez, dans votre modélisation des données, aux données dont vos clients ont besoin, ce qui influencera grandement le schéma de données que vous développerez pour votre projet.
Ce guide commence par un nouveau schéma pour les avis sur les films, puis couvre les requêtes et les mutations dérivées de ce schéma, et enfin fournit une liste SQL équivalente au schéma de base Data Connect.
Schéma d'une application d'avis sur des films
Imaginez que vous souhaitez créer un service permettant aux utilisateurs d'envoyer et de consulter des avis sur des films.
Vous avez besoin d'un schéma initial pour une telle application. Vous étendrez ce schéma ultérieurement pour créer des requêtes relationnelles complexes.
Table "Movie"
Le schéma des films contient des directives de base telles que :
@table, qui nous permet de définir des noms d'opération à l'aide des argumentssingularetplural@colpour définir explicitement les noms de colonnes@defaultpour autoriser la définition de valeurs par défaut.
# 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")
}
Valeurs de serveur et scalaires clés
Avant d'examiner l'application d'avis sur des films, présentons les Data Connect valeurs de serveur et les scalaires clés de Data Connect.
À l'aide des valeurs de serveur, vous pouvez laisser le serveur remplir dynamiquement les
champs de vos tables à l'aide de valeurs stockées ou facilement calculables en fonction d'expressions
côté serveur spécifiques. Par exemple, vous pouvez définir un champ avec un
code temporel appliqué lorsque le champ est accessible à l'aide de l'expression
updatedAt: Timestamp! @default(expr: "request.time").
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 sont axés sur l'efficacité, car 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 supplémentaires plus complexes.
Type d'ID
Dans GraphQL, le type ID est défini comme un type opaque sérialisé en tant que chaîne. GraphQL est indépendant du format d'ID, mais il forcera les chaînes et les entiers à partir de l'entrée.
Les clés PostgreSQL sont généralement des entiers ou des UUID, et non des chaînes.
Data Connect génère automatiquement ces clés à partir de votre schéma. Vous
pouvez personnaliser la génération de clés avec la @default directive, comme indiqué dans la définition du champ id de la table "Actor"
: id: ID! … @default(generate: "UUID").
Table de métadonnées de films
Nous allons maintenant suivre les réalisateurs de films et configurer une relation un-à-un avec Movie.
Ajoutez la directive @ref pour définir des relations.
# 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")
}
Acteur et MovieActor
Ensuite, vous souhaitez que des acteurs jouent dans vos films. Comme vous avez une relation 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(name: "Actors", singular: "actor", plural: "actors") {
id: UUID! @col(name: "actor_id") @default(expr: "uuidV4()")
name: String! @col(name: "name", dataType: "varchar(30)")
}
# Join table for many-to-many relationship for movies and actors
# The 'key' param signifies the primary key(s) of this table
# In this case, the keys are [movieId, actorId], the generated fields of the reference types [movie, actor]
type MovieActor @table(key: ["movie", "actor"]) {
# @ref creates a field in the current table (MovieActor) 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, see: implicit.gql
actor: Actor! @ref
# actorId: UUID! <- this is created by the above @ref, see: implicit.gql
role: String! @col(name: "role") # "main" or "supporting"
# optional other fields
}
Utilisateur
Enfin, les utilisateurs de votre application.
# Users
# Suppose a user can leave reviews for movies
# user:reviews is a one to many relationship, movie:reviews is a one to many relationship, movie:user is a many to many relationship
type User
@table(name: "Users", singular: "user", plural: "users", key: ["id"]) {
id: UUID! @col(name: "user_id") @default(expr: "uuidV4()")
auth: String @col(name: "user_auth") @default(expr: "auth.uid")
username: String! @col(name: "username", dataType: "varchar(30)")
# The following are generated from the @ref in the Review table
# reviews_on_user
# movies_via_Review
}
Types de données acceptés
Data Connect est compatible avec les types de données scalaires suivants, avec
des affectations aux types PostgreSQL à l'aide de @col(dataType:).
| Data Connect type | Type intégré GraphQL ou Data Connect type personnalisé |
Type PostgreSQL par défaut | Types PostgreSQL acceptés (alias entre parenthèses) |
|---|---|---|---|
| Chaîne | GraphQL | texte | texte bit(n), varbit(n) char(n), varchar(n) |
| Int | GraphQL | int | Int2 (smallint, smallserial), int4 (integer, int, serial) |
| Float | GraphQL | float8 | float4 (real) float8 (double precision) numeric (decimal) |
| Booléen | GraphQL | booléen | booléen |
| UUID | Personnalisé | uuid | uuid |
| Int64 | Personnalisé | bigint | int8 (bigint, bigserial) numeric (decimal) |
| Date | Personnalisé | date | date |
| Horodatage | Personnalisé | timestamptz | timestamptz Remarque : Les informations sur le fuseau horaire local ne sont pas stockées. |
| Énumération | Personnalisé | enum | enum |
| Vecteur | Personnalisé | vecteur | vecteur Consultez Effectuer une recherche de similarité vectorielle avec Vertex AI. |
- GraphQL
Listcorrespond à un tableau unidimensionnel.- Par exemple,
[Int]correspond àint5[], et[Any]correspond àjsonb[]. - Data Connect n'est pas compatible avec les tableaux imbriqués.
- Par exemple,
Requêtes et mutations implicites et prédéfinies
Vos Data Connect requêtes et mutations étendront un ensemble de requêtes implicites et de mutations implicites générées par Data Connect en fonction des types et des relations de types dans votre schéma. Les requêtes et mutations implicites sont générées par des outils locaux chaque fois que vous modifiez votre schéma.
Dans votre processus de développement, vous implémenterez des requêtes prédéfinies et des mutations prédéfinies basées sur ces opérations implicites.
Nommage implicite des requêtes et des mutations
Data Connect déduit des noms appropriés pour les requêtes et les mutations implicites
à partir des déclarations de type de votre schéma. Par exemple, si vous utilisez une source PostgreSQL et que vous définissez une table nommée Movie, le serveur générera des éléments implicites :
- Requêtes pour les cas d'utilisation d'une seule table avec les noms conviviaux
movie(singulier, pour récupérer des résultats individuels en transmettant des arguments tels queeq) etmovies(pluriel, pour récupérer des listes de résultats en transmettant des arguments tels quegtet des opérations telles queorderby). Data Connect génère également des requêtes pour les opérations relationnelles multi-tables, avec des noms explicites tels queactors_on_moviesouactors_via_actormovie. - Mutations nommées
movie_insert,movie_upsert, etc.
Le langage de définition de schéma vous permet également de définir explicitement des noms pour les opérations à l'aide des arguments de directive singular et plural.
Requêtes pour la base de données d'avis sur des 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 démarrage rapide, l'exemple de requête listEmails ne prenait aucun paramètre. Bien sûr, 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 :
- Une définition de type
query - Un nom d'opération (requête)
ListMoviesByGenre - Un seul argument d'opération
$genre - Une seule directive,
@auth.
query ListMoviesByGenre($genre: String!) @auth(level: USER)
Chaque argument de requête 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.
Examinons la signature des requêtes de plus en plus complexes. Vous terminerez par la présentation d'expressions de relation puissantes et concises disponibles dans les requêtes implicites sur lesquelles vous pouvez vous appuyer dans vos requêtes prédéfinies.
Scalaires clés dans les requêtes
Mais tout d'abord, une remarque sur les scalaires clés.
Data Connect définit un type spécial pour les scalaires clés, identifié par
_Key. Par exemple, le type d'un scalaire clé pour notre Movie table est
Movie_Key.
Vous récupérez les scalaires clés en tant que réponse renvoyée par la plupart des mutations implicites, ou bien à partir de requêtes dans 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 clé.
Vous pouvez transmettre un scalaire clé en tant que littéral. Toutefois, vous pouvez définir des variables pour transmettre des scalaires clés en entrée.
query GetMovie($myKey: Movie_Key!) {
movie(key: $myKey) { title }
}
Vous pouvez les fournir dans une 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. Cela est surtout utile lorsque vous souhaitez diviser des composants individuels en différentes variables pour une raison quelconque.
Alias dans les requêtes
Data Connect est compatible avec les alias GraphQL dans les requêtes. Avec les alias, vous renommez les données renvoyées dans les résultats d'une requête. Une seule Data Connect requête peut appliquer plusieurs filtres ou d'autres opérations de requête dans une seule requête efficace au serveur, en émettant plusieurs "sous-requêtes" à la fois. Pour éviter les conflits de noms dans l'ensemble de données renvoyé, vous utilisez 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
Data Connect requêtes correspondent à tous les filtres SQL courants et aux opérations d'ordre.
Opérateurs where et orderBy (requêtes singulières et plurielles)
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 singulières et plurielles)
Vous pouvez effectuer une pagination sur les 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
}
}
includes pour les champs de tableau
Vous pouvez tester si 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 classiques, y compris des expressions régulières. Notez que, pour plus d'efficacité, vous regroupez ici plusieurs opérations et les distinguez à 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
Data Connect requêtes 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 récupérer des données pour un type ainsi que des données d'un type imbriqué ou associé.
Ces requêtes utilisent la syntaxe magique Data Connect _on_ et _via dans les requêtes implicites générées.
Vous allez apporter des modifications au schéma à partir de notre version initiale.
Plusieurs-à-un
Ajoutons des avis à notre application, avec une table Review et des modifications à User.
# Users
# Suppose a user can leave reviews for movies
# user:reviews is a one to many relationship,
# movie:reviews is a one to many relationship,
# movie:user is a many to many relationship
type User
@table(name: "Users", singular: "user", plural: "users", key: ["id"]) {
id: UUID! @col(name: "user_id") @default(expr: "uuidV4()")
auth: String @col(name: "user_auth") @default(expr: "auth.uid")
username: String! @col(name: "username", dataType: "varchar(30)")
# The following are generated from the @ref in the Review table
# reviews_on_user
# movies_via_Review
}
# Reviews
type Review @table(name: "Reviews", key: ["movie", "user"]) {
id: UUID! @col(name: "review_id") @default(expr: "uuidV4()")
user: User! @ref
movie: Movie! @ref
rating: Int
reviewText: String
reviewDate: Date! @default(expr: "request.time")
}
Requête pour plusieurs-à-un
Examinons 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
description
}
dislikedMovies: movies_via_review(where: { rating: { le: 2 } }) {
title
genre
description
}
}
}
Un-à-un
Vous voyez le modèle. Ci-dessous, le schéma est modifié à titre 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 un-à-un
Vous pouvez interroger à 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 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 pour 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
}
}
}
Mutations pour la base de données d'avis sur des films
Comme mentionné précédemment, lorsque vous définissez une table dans votre schéma, Data Connect générera des mutations implicites 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
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 insertion/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 des mises à jour. Les producteurs et les réalisateurs espèrent certainement que ces notes moyennes sont en tendance.
mutation UpdateMovie(
$id: UUID!,
$genre: String!,
$rating: Int!,
$description: String!
) {
movie_update(id: $id, data: {
genre: $genre
rating: $rating
description: $description
})
}
# Multiple updates (increase all ratings of a genre)
mutation IncreaseRatingForGenre($genre: String!, $ratingIncrement: Int!) {
movie_updateMany(
where: { genre: { eq: $genre } },
update: { rating: { inc: $ratingIncrement } }
)
}
Effectuer des suppressions
Vous pouvez bien sûr supprimer des données de films. Les spécialistes de la préservation 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 ici.
# Multiple deletes
mutation DeleteUnpopularMovies($minRating: Int!) {
movie_deleteMany(where: { rating: { le: $minRating } })
}
Écrire des mutations sur les relations
Découvrez comment utiliser la mutation implicite _upsert 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 }
)
}
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 appeler vos requêtes et mutations à partir d'un SDK Web, d'un SDK Android, d'un SDK iOS et d'un SDK Flutter générés automatiquement.