Schémas, requêtes et 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 d'un schéma, de requêtes et de mutations permettant d'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 les requêtes et mutations Data Connect à des exemples de schéma. Pourquoi parler des requêtes (et des mutations) dans un guide sur les schémas Data Connect ? 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. En tant que développeur, vous devrez donc réfléchir aux données dont vos clients ont besoin dans votre modélisation des données, ce qui 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, couvre les requêtes et les mutations dérivées de ce schéma, et fournit 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, qui nous permet de définir des noms d'opération à l'aide des arguments singular et plural
  • @col pour définir explicitement les noms des colonnes
  • @default pour 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 de clé

Avant d'examiner l'application d'évaluation de films, examinons les valeurs serveur Data Connect et les valeurs scalaires clés.

À l'aide des 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 côté serveur particulières. Par exemple, vous pouvez définir un champ avec un code temporel appliqué lorsque le champ est consulté à l'aide de l'expression updatedAt: Timestamp! @default(expr: "request.time").

Les valeurs scalaires de clé sont des identifiants d'objets 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 avez besoin d'un identifiant unique à transmettre aux opérations à venir, ainsi que lorsque vous souhaitez accéder à des clés relationnelles pour effectuer d'autres opérations plus complexes.

Table des métadonnées des films

Voyons maintenant comment suivre les réalisateurs de films et configurer une relation individuelle 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 les acteurs jouent dans vos films et, comme vous avez une relation de type 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 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, série)
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.
PostgreSQL convertit et stocke ces codes temporels au format UTC.

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] correspond à jsonb[].
    • Data Connect n'est pas compatible avec les tableaux imbriqués.

Requêtes et mutations implicites et prédéfinies

Vos requêtes et mutations Data Connect étendent 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 type dans votre schéma. Les requêtes et les mutations implicites sont générées par les outils locaux chaque fois que vous modifiez votre schéma.

Au cours du processus de développement, vous implémenterez des requêtes prédéfinies et des mutations prédéfinies en fonction de ces opérations implicites.

Noms des requêtes et des mutations implicites

Data Connect déduit les noms appropriés pour les requêtes et mutations implicites à partir 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 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 que eq) et movies (pluriel, pour récupérer des listes de résultats en transmettant des arguments tels que gt et des opérations telles que orderby). Data Connect génère également des requêtes pour des opérations relationnelles multitables avec des noms explicites tels que actors_on_movies ou actors_via_actormovie.
  • Mutations nommées movie_insert, movie_upsert

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 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.

Ainsi, la requête suivante a:

  • Définition de type query
  • Un nom d'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 schéma comme Movie.

Examinons la signature des requêtes de plus en plus complexes. Vous terminerez par présenter des expressions de relation puissantes et concises disponibles dans les requêtes implicites que vous pouvez développer dans vos requêtes prédéfinies.

Clés scalaires dans les requêtes

Mais d'abord, une remarque sur les scalaires clés.

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 des scalaires clés en tant que réponse renvoyée par la plupart des mutations implicites, 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 exécuté, prennent en charge un argument 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 clés en entrée.

query GetMovie($myKey: Movie_Key!) {
  movie(key: $myKey) { title }
}

Celles-ci peuvent être fournies dans une requête JSON comme celle-ci (ou dans d'autres formats de sérialisation):

{
  # 
  "variables": {
    "myKey": {"foo": "some-string-value", "bar": 42}
  }
}

Grâce à l'analyse scalaire personnalisée, une Movie_Key peut également être construite à 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 requêtes implicites générées.

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 dans 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 de type "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
      description
    }
    dislikedMovies: movies_via_review(where: { rating: { le: 2 } }) {
      title
      genre
      description
    }
  }
}

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

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 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. Répète ça cinq fois vite !

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.

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 les données de film. Les conservateurs de films voudront certainement que les films physiques soient conservés aussi longtemps que 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 }
  )
}

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