Data Connect-Schemas, -Abfragen und -Mutationen

Mit Firebase Data Connect können Sie Connectors für Ihre mit Google Cloud SQL verwalteten PostgreSQL-Instanzen erstellen. Diese Connectors sind Kombinationen aus einem Schema, Abfragen und Mutationen zur Verwendung Ihrer Daten.

Im Einstiegsleitfaden wurde ein Schema für eine Filmbewertungs-App für PostgreSQL vorgestellt. In diesem Leitfaden geht es genauer darum, wie Sie Data Connect-Schemas für PostgreSQL entwerfen.

In diesem Leitfaden werden Data Connect-Abfragen und ‑Mutationen mit Schemabeispielen kombiniert. Warum werden Abfragen (und Änderungen) in einem Leitfaden zu Data Connect-Schemas besprochen? Wie andere GraphQL-basierte Plattformen ist Firebase Data Connect eine Abfrage-first-Entwicklungsplattform. Als Entwickler müssen Sie bei der Datenmodellierung also an die Daten denken, die Ihre Kunden benötigen. Das hat großen Einfluss auf das Datenschema, das Sie für Ihr Projekt entwickeln.

Dieser Leitfaden beginnt mit einem neuen Schema für Filmrezensionen, behandelt dann die aus diesem Schema abgeleiteten Abfragen und Mutationen und enthält schließlich ein SQL-Listenelement, das dem Data Connect-Kernschema entspricht.

Das Schema für eine Filmkritik-App

Angenommen, Sie möchten einen Dienst erstellen, mit dem Nutzer Filmrezensionen einreichen und ansehen können.

Für eine solche App benötigen Sie ein Anfangsschema, das Sie später erweitern, um komplexe relationale Abfragen zu erstellen.

Filmtabelle

Das Schema für Filme enthält wichtige Anweisungen wie:

  • @table(name) und @col(name) zum Anpassen der SQL-Tabellen- und Spaltennamen. Wenn Sie keine Namen angeben, generiert Data Connect Namen im Snake Case.
  • @col(dataType), um SQL-Spaltentypen anzupassen.
  • @default, um beim Einfügen Standardwerte für SQL-Spalten zu konfigurieren.

Weitere Informationen finden Sie in den Referenzdokumenten zu @table, @col und @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
}

Wichtige Skalare und Serverwerte

Bevor wir uns die Filmbewertungs-App genauer ansehen, möchten wir Data Connect Schlüsselskaläre und Serverwerte vorstellen.

Schlüsselskalare sind kompakte Objekt-IDs, die von Data Connect automatisch aus Schlüsselfeldern in Ihren Schemas zusammengestellt werden. Schlüsselskaläre Werte stehen für Effizienz. So können Sie mit einem einzigen Aufruf Informationen zur Identität und Struktur Ihrer Daten abrufen. Sie sind besonders nützlich, wenn Sie sequenzielle Aktionen auf neue Einträge ausführen und eine eindeutige Kennung für nachfolgende Vorgänge benötigen. Außerdem sind sie nützlich, wenn Sie auf relationale Schlüssel zugreifen möchten, um zusätzliche, komplexere Vorgänge auszuführen.

Mit Serverwerten können Sie den Server effektiv dazu veranlassen, Felder in Ihren Tabellen dynamisch mit gespeicherten oder leicht berechenbaren Werten gemäß bestimmten serverseitigen CEL-Ausdrücken im expr-Argument zu füllen. Sie können beispielsweise ein Feld mit einem Zeitstempel definieren, der beim Zugriff auf das Feld mit der in einer Vorgangsanfrage (updatedAt: Timestamp! @default(expr: "request.time")) gespeicherten Zeit angewendet wird.

Tabelle mit Filmmetadaten

Sehen wir uns nun an, wie wir Filmregisseure im Blick behalten und eine persönliche Beziehung zu Movie aufbauen.

Fügen Sie das Referenzfeld hinzu, um Beziehungen zu definieren.

Mit der Direktive @ref können Sie die Fremdschlüsseleinschränkung anpassen.

  • @ref(fields), um die Fremdschlüsselfelder anzugeben.
  • @ref(references), um die Felder anzugeben, auf die in der Zieltabelle verwiesen wird. Standardmäßig wird der Primärschlüssel verwendet, aber auch Felder mit @unique werden unterstützt.

Weitere Informationen finden Sie in der Referenzdokumentation zu @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
}

Schauspieler und Filmschauspieler

Als Nächstes möchten Sie Schauspieler in Ihren Filmen haben. Da zwischen Filmen und Schauspielern eine Beziehung vom Typ „Mehrfach zueinander“ besteht, erstellen Sie eine Join-Tabelle.

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

Nutzer

Und schließlich die Nutzer Ihrer App.

# Users
# Suppose a user can leave reviews for movies
type User @table {
  id: String! @default(expr: "auth.uid")
  username: String! @col(dataType: "varchar(50)")
}

Unterstützte Datentypen

Data Connect unterstützt die folgenden skalaren Datentypen, wobei PostgreSQL-Typen mit @col(dataType:) zugewiesen werden.

Data Connect-Typ Integrierter GraphQL-Typ oder
Data Connect benutzerdefinierter Typ
Standard-PostgreSQL-Typ Unterstützte PostgreSQL-Typen
(Alias in Klammern)
String GraphQL Text text
bit(n), varbit(n)
char(n), varchar(n)
Integer GraphQL int Int2 (smallint, smallserial),
int4 (integer, int, serial)
Float GraphQL float8 float4 (real)
float8 (double precision)
numeric (decimal)
Boolesch GraphQL boolean boolean
UUID Benutzerdefiniert uuid uuid
INT64 Benutzerdefiniert bigint int8 (bigint, bigserial)
numeric (dezimal)
Datum Benutzerdefiniert Datum Datum
Zeitstempel Benutzerdefiniert timestamptz

timestamptz

Hinweis:Informationen zur lokalen Zeitzone werden nicht gespeichert.
PostgreSQL wandelt solche Zeitstempel in UTC um und speichert sie.

Vektor Benutzerdefiniert vector

Vektor

Weitere Informationen finden Sie unter Mit Vertex AI nach Vektorähnlichkeiten suchen.

  • GraphQL List wird einem eindimensionalen Array zugeordnet.
    • Beispiel: [Int] wird int5[] zugeordnet, [Any] wird jsonb[] zugeordnet.
    • Data Connect unterstützt keine verschachtelten Arrays.

Generierte Felder zum Erstellen von Abfragen und Mutationen verwenden

Ihre Data Connect-Abfragen und Data Connect-Mutationen erweitern eine Reihe von Feldern, die basierend auf den Typen und den Typenbeziehungen in Ihrem Schema automatisch generiert werden. Diese Felder werden von lokalen Tools generiert, wenn Sie Ihr Schema bearbeiten.

  • Wie Sie im Leitfaden „Einstieg“ erfahren haben, werden in der Firebase-Konsole und in unseren Tools zur lokalen Entwicklung diese automatisch generierten Felder verwendet, um Ihnen Ad-hoc-Verwaltungsabfragen und ‑Änderungen zur Verfügung zu stellen, mit denen Sie Daten einfügen und den Inhalt Ihrer Tabellen überprüfen können.

  • Im Rahmen Ihres Entwicklungsvorgangs implementieren Sie bereitstellungsfähige Abfragen und bereitstellungsfähige Mutationen, die in Ihren Konnektoren gebündelt sind und auf diesen automatisch generierten Feldern basieren.

Namen für automatisch generierte Felder

Data Connect leitet geeignete Namen für Felder ab, die basierend auf Ihren Schematypdeklarationen automatisch generiert werden. Wenn Sie beispielsweise mit einer PostgreSQL-Quelle arbeiten und eine Tabelle mit dem Namen Movie definieren, generiert der Server:

  • Felder zum Lesen von Daten in Anwendungsfällen mit einer einzelnen Tabelle mit den freundlichen Namen movie (Singular, zum Abrufen einzelner Ergebnisse, wobei Argumente wie eq übergeben werden) und movies (Plural, zum Abrufen von Ergebnislisten, wobei Argumente wie gt und Vorgänge wie orderby übergeben werden). Data Connect generiert auch Felder für relationale Vorgänge mit mehreren Tabellen mit expliziten Namen wie actors_on_movies oder actors_via_actormovie.
  • Felder zum Schreiben von Daten mit bekannten Namen wie movie_insert, movie_upsert

Mit der Schemadefinitionsprache können Sie auch explizit steuern, wie Namen für Felder mithilfe von singular- und plural-Anweisungsargumenten generiert werden.

Richtlinien für Abfragen und Mutationen

Zusätzlich zu den Anweisungen, die Sie zum Definieren von Typen und Tabellen verwenden, bietet Data Connect die Anweisungen @auth, @check, @redact und @transaction, mit denen Sie das Verhalten von Abfragen und Mutationen anpassen können.

Anweisung Gilt für Beschreibung
@auth Abfragen und Mutationen Definiert die Autorisierungsrichtlinie für eine Abfrage oder Mutation. Weitere Informationen finden Sie im Leitfaden zur Autorisierung und Attestierung.
@check query-Felder in mehrstufigen Vorgängen Prüft, ob die angegebenen Felder in den Abfrageergebnissen vorhanden sind. Feldwerte werden mit einem CEL-Ausdruck (Common Expression Language) getestet. Weitere Informationen finden Sie unter Mehrere Schritte.
@redact Abfragen Ein Teil der Antwort des Clients wird entfernt. Weitere Informationen finden Sie unter Mehrere Schritte.
@transaction Mutationen Erzwingt, dass eine Mutation immer in einer Datenbanktransaktion ausgeführt wird. Weitere Informationen finden Sie unter Mehrere Schritte.

Abfragen für die Datenbank mit Filmrezensionen

Sie definieren eine Data Connect-Abfrage mit einer Deklaration des Abfragevorgangstyps, dem Namen des Vorgangs, null oder mehr Vorgangsargumenten und null oder mehr Anweisungen mit Argumenten.

In der Kurzanleitung wurden für die Beispielabfrage listEmails keine Parameter verwendet. In vielen Fällen sind die an Abfragefelder übergebenen Daten natürlich dynamisch. Mit der $variableName-Syntax können Sie Variablen als eine der Komponenten einer Abfragedefinition verwenden.

Die folgende Abfrage enthält daher:

  • Eine query-Typdefinition
  • Name eines ListMoviesByGenre-Vorgangs (Abfrage)
  • Ein einzelnes Argument für den $genre-Vorgang
  • Eine einzelne Anweisung, @auth.
query ListMoviesByGenre($genre: String!) @auth(level: USER)

Für jedes Abfrageargument ist eine Typdeklaration erforderlich, z. B. ein vordefinierter Typ wie String oder ein benutzerdefinierter, schemadefinierter Typ wie Movie.

Sehen wir uns die Signatur zunehmend komplexer Abfragen an. Zum Schluss werden leistungsstarke, prägnante Beziehungsausdrücke vorgestellt, mit denen Sie Ihre implementierbaren Abfragen erstellen können.

Wichtige Skalare in Abfragen

Aber zuerst ein Hinweis zu wichtigen Skalaren.

Data Connect definiert einen speziellen Typ für Schlüsselskalare, die durch _Key gekennzeichnet sind. Der Typ eines Schlüsselskalaars für unsere Tabelle Movie ist beispielsweise Movie_Key.

Schlüsselskaläre werden als Antwort von den meisten automatisch generierten Lesefeldern zurückgegeben oder natürlich aus Abfragen, in denen Sie alle Felder abgerufen haben, die zum Erstellen des skalaren Schlüssels erforderlich sind.

Einzelne automatische Abfragen wie movie in unserem Beispiel unterstützen ein Schlüsselargument, das einen Schlüsselskalar akzeptiert.

Sie können einen Schlüsselskalar als Literal übergeben. Sie können jedoch Variablen definieren, um wichtige Skalare als Eingabe zu übergeben.

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

Sie können in einer JSON-Anfrage wie dieser (oder in anderen Serialization-Formaten) angegeben werden:

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

Dank benutzerdefinierter Skalarparsierung kann ein Movie_Key auch mit der Objektsyntax erstellt werden, die Variablen enthalten kann. Das ist vor allem dann nützlich, wenn Sie einzelne Komponenten aus irgendeinem Grund in verschiedene Variablen aufteilen möchten.

Aliasse in Abfragen

Data Connect unterstützt das Aliasing von GraphQL in Abfragen. Mit Aliassen können Sie die Daten umbenennen, die in den Ergebnissen einer Abfrage zurückgegeben werden. Mit einer einzelnen Data Connect-Abfrage können mehrere Filter oder andere Abfragevorgänge in einer einzigen effizienten Anfrage an den Server angewendet werden, wodurch mehrere „Unterabfragen“ gleichzeitig ausgeführt werden. Um Namenskollisionen im zurückgegebenen Datensatz zu vermeiden, verwenden Sie Aliasse, um die Unterabfragen zu unterscheiden.

Hier ist eine Abfrage, in der ein Ausdruck den Alias mostPopular verwendet.

query ReviewTopPopularity($genre: String) {
  mostPopular: review(first: {
    where: {genre: {eq: $genre}},
    orderBy: {popularity: DESC}
  }) {  }
}

Einfache Abfragen mit Filtern

Data Connect-Abfragen werden allen gängigen SQL-Filtern und -Sortiervorgängen zugeordnet.

where- und orderBy-Operatoren (Singular- und Pluralabfragen)

Gibt alle übereinstimmenden Zeilen aus der Tabelle (und verschachtelte Verknüpfungen) zurück. Gibt ein leeres Array zurück, wenn keine Datensätze mit dem Filter übereinstimmen.

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}]) {  }
}

limit- und offset-Operatoren (Singular- und Pluralabfragen)

Sie können die Ergebnisse paginaieren. Diese Argumente werden akzeptiert, aber nicht in den Ergebnissen zurückgegeben.

query MoviesTop10 {
  movies(orderBy: [{ rating: DESC }], limit: 10) {
    # graphql: list the fields from the results to return
    title
  }
}

„Umfasst“ für Arrayfelder

Sie können prüfen, ob ein Arrayfeld ein bestimmtes Element enthält.

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

String-Operationen und reguläre Ausdrücke

Für Abfragen können typische Stringsuch- und Vergleichsvorgänge verwendet werden, einschließlich regulärer Ausdrücke. Hinweis: Aus Effizienzgründen bündeln Sie hier mehrere Vorgänge und trennen sie mit Aliasen.

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 und and für zusammengesetzte Filter

Verwenden Sie or und and für komplexere Logik.

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

Komplexe Abfragen

Data Connect-Abfragen können auf Daten basierend auf den Beziehungen zwischen Tabellen zugreifen. Sie können die in Ihrem Schema definierten Objektbeziehungen (1:1) oder Arraybeziehungen (1:n) verwenden, um verschachtelte Abfragen auszuführen, d.h. Daten für einen Typ zusammen mit Daten aus einem verschachtelten oder ähnlichen Typ abzurufen.

Bei solchen Abfragen wird in generierten Lesefeldern die magische Data Connect-, _on_- und _via-Syntax verwendet.

Sie nehmen Änderungen am Schema aus unserer ursprünglichen Version vor.

Viele-zu-Eins

Fügen wir unserer App Rezensionen mit einer Review-Tabelle und Änderungen an User hinzu.

# 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")
}

Abfrage für „Viele zu Eins“

Sehen wir uns nun eine Abfrage mit Alias an, um die _via_-Syntax zu veranschaulichen.

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

Einzelgespräch

Sie erkennen das Muster. Unten ist das Schema zur Veranschaulichung geändert.

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

Abfrage für Einzelgespräch

Sie können Abfragen mit der _on_-Syntax stellen.

# One to one
query GetMovieMetadata($id: UUID!) @auth(level: PUBLIC) {
  movie(id: $id) {
    movieMetadatas_on_movie {
      director
    }
  }
}

Viele zu viele

Filme brauchen Schauspieler und Schauspieler brauchen Filme. Sie stehen in einer m:n-Beziehung, die Sie mit einer MovieActors-Join-Tabelle modellieren können.

# 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!]!
}

Abfrage für „Viele zu viele“

Sehen wir uns eine Abfrage mit Alias an, um die _via_-Syntax zu veranschaulichen.

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

Aggregationsabfragen

Was sind Aggregate und warum sollten Sie sie verwenden?

Mit Aggregierungsfeldern können Sie Berechnungen für eine Liste von Ergebnissen durchführen. Mit Summenfeldern haben Sie folgende Möglichkeiten:

  • Durchschnittliche Bewertung einer Rezension ermitteln
  • Gesamtkosten der Artikel in einem Einkaufswagen ermitteln
  • Das am besten oder am schlechtesten bewertete Produkt finden
  • Anzahl der Produkte in Ihrem Geschäft zählen

Aggregationen werden auf dem Server ausgeführt. Das bietet einige Vorteile gegenüber der clientseitigen Berechnung:

  • Höhere App-Leistung, da clientseitige Berechnungen vermieden werden
  • Geringere Kosten für den Datenausgang, da nur die aggregierten Ergebnisse statt aller Eingaben gesendet werden
  • Verbesserte Sicherheit, da Sie Kunden Zugriff auf aggregierte Daten statt auf den gesamten Datensatz gewähren können

Beispielschema für Aggregate

In diesem Abschnitt wechseln wir zu einem Beispielschema für einen Store, das sich gut zur Erklärung der Verwendung von Aggregaten eignet:

  type Product @table {
    name: String!
    manufacturer: String!
    quantityInStock: Int!
    price: Float!
    expirationDate: Date
  }

Einfache Aggregate

_count für alle Felder

Das einfachste Aggregatfeld ist _count. Es gibt an, wie viele Zeilen mit Ihrer Abfrage übereinstimmen. Für jedes Feld in Ihrem Typ generiert Data Connect je nach Feldtyp entsprechende Aggregationsfelder.

Abfrage

query CountProducts {
  products {
    _count
  }
}

Antwort
one

Wenn Sie beispielsweise 5 Produkte in Ihrer Datenbank haben, würde das Ergebnis so aussehen:

{
  "products": [
    {
    "_count": 5
    }
  ]
}

Alle Felder haben ein Feld vom Typ <field>_count, in dem gezählt wird, wie viele Zeilen einen nicht nullwertigen Wert in diesem Feld haben.

Abfrage

query CountProductsWithExpirationDate {
  products {
    expirationDate_count
  }
}

Antwort
field_count

Wenn Sie beispielsweise drei Produkte mit einem Ablaufdatum haben, würde das Ergebnis so aussehen:

{
  "products": [
    {
    "expirationDate_count": 3
    }
  ]
}
_min, _max, _sum und _avg für numerische Felder

Für numerische Felder (int, float, int64) sind auch <field>_min, <field>_max, <field>_sum und <field>_avg zulässig.

Abfrage

query NumericAggregates {
  products {
  quantityInStock_max
  price_min
  price_avg
  quantityInStock_sum
  }
}

Antwort
_min _max _sum _avg

Angenommen, Sie haben die folgenden Produkte:

  • Produkt A: quantityInStock: 10, price: 2.99
  • Produkt B: quantityInStock: 5, price: 5.99
  • Produkt C: quantityInStock: 20, price: 1.99

Das Ergebnis wäre:

{
  "products": [
    {
    "quantityInStock_max": 20,
    "price_min": 1.99,
    "price_avg": 3.6566666666666666,
    "quantityInStock_sum": 35
    }
  ]
}
_min und _max für Datumsangaben und Zeitstempel

Die Felder für Datum und Zeitstempel haben <field>_min und <field>_max.

Abfrage

query DateAndTimeAggregates {
  products {
  expirationDate_max
  expirationDate_min
  }
}

Antwort
_min _maxdatetime

Angenommen, Sie haben die folgenden Ablaufdaten:

  • Produkt A: 2024-01-01
  • Produkt B: 2024-03-01
  • Produkt C: 2024-02-01

Das Ergebnis wäre:

{
  "products": [
    {
    "expirationDate_max": "2024-03-01",
    "expirationDate_min": "2024-01-01"
    }
  ]
}

Distinct

Mit dem Argument distinct können Sie alle eindeutigen Werte für ein Feld (oder eine Kombination von Feldern) abrufen. Beispiel:

Abfrage

query ListDistinctManufacturers {
  products(distinct: true) {
    manufacturer
  }
}

Antwort
distinct

Angenommen, Sie haben die folgenden Hersteller:

  • Produkt A: manufacturer: "Acme"
  • Produkt B: manufacturer: "Beta"
  • Produkt C: manufacturer: "Acme"

Das Ergebnis wäre:

{
  "products": [
    { "manufacturer": "Acme" },
    { "manufacturer": "Beta" }
  ]
}

Sie können das Argument distinct auch in Aggregierungsfeldern verwenden, um stattdessen die einzelnen Werte zu aggregieren. Beispiel:

Abfrage

query CountDistinctManufacturers {
  products {
    manufacturer_count(distinct: true)
  }
}

Antwort
distinctonaggregate

Angenommen, Sie haben die folgenden Hersteller:

  • Produkt A: manufacturer: "Acme"
  • Produkt B: manufacturer: "Beta"
  • Produkt C: manufacturer: "Acme"

Das Ergebnis wäre:

{
  "products": [
    {
    "manufacturer_count": 2
    }
  ]
}

Gruppierte Aggregate

Wenn Sie eine gruppierte Aggregation ausführen möchten, wählen Sie eine Mischung aus Aggregations- und Nichtaggregationsfeldern für einen Typ aus. Dadurch werden alle übereinstimmenden Zeilen mit demselben Wert für die nicht aggregierten Felder zusammengefasst und die aggregierten Felder für diese Gruppe berechnet. Beispiel:

Abfrage

query MostExpensiveProductByManufacturer {
  products {
  manufacturer
  price_max
  }
}

Antwort
groupedaggregates

Angenommen, Sie haben die folgenden Produkte:

  • Produkt A: manufacturer: "Acme", price: 2.99
  • Produkt B: manufacturer: "Beta", price: 5.99
  • Produkt C: manufacturer: "Acme", price: 1.99

Das Ergebnis wäre:

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}
having und where mit gruppierten Aggregaten

Sie können auch die Argumente having und where verwenden, um nur Gruppen zurückzugeben, die bestimmte Kriterien erfüllen.

  • Mit having können Sie Gruppen nach ihren Summenfeldern filtern.
  • Mit where können Sie die Zeilen anhand von nicht aggregierten Feldern filtern.

Abfrage

query FilteredMostExpensiveProductByManufacturer {
  products(having: {price_max: {ge: 2.99}}) {
  manufacturer
  price_max
  }
}

Antwort
havingwhere

Angenommen, Sie haben die folgenden Produkte:

  • Produkt A: manufacturer: "Acme", price: 2.99
  • Produkt B: manufacturer: "Beta", price: 5.99
  • Produkt C: manufacturer: "Acme", price: 1.99

Das Ergebnis wäre:

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}

Aggregiert Daten aus mehreren Tabellen

Aggregierungsfelder können in Kombination mit generierten Feldern für eine Eins-zu-Viele-Beziehung verwendet werden, um komplexe Fragen zu Ihren Daten zu beantworten. Hier ist ein geändertes Schema mit einer separaten Tabelle Manufacturer, die wir in Beispielen verwenden können:

  type Product @table {
    name: String!
    manufacturer: Manufacturer!
    quantityInStock: Int!
    price: Float!
    expirationDate: Date
  }

  type Manufacturer @table {
    name: String!
    headquartersCountry: String!
  }

Mithilfe von zusammengefassten Feldern können wir beispielsweise ermitteln, wie viele Produkte ein Hersteller herstellt:

Abfrage

query GetProductCount($id: UUID) {
  manufacturers {
    name
    products_on_manufacturer {
      _count
    }
  }
}

Antwort
aggregatesacrosstables

Angenommen, Sie haben die folgenden Hersteller:

  • Hersteller A: name: "Acme", products_on_manufacturer: 2
  • Hersteller B: name: "Beta", products_on_manufacturer: 1

Das Ergebnis wäre:

{
  "manufacturers": [
    { "name": "Acme", "products_on_manufacturer": { "_count": 2 } },
    { "name": "Beta", "products_on_manufacturer": { "_count": 1 } }
  ]
}

Mutationen für die Datenbank mit Filmrezensionen

Wie bereits erwähnt, generiert Data Connect für jede Tabelle grundlegende Felder vom Typ _insert, _update usw., wenn Sie eine Tabelle in Ihrem Schema definieren.

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

So können Sie immer komplexere CRUD-Grundfälle implementieren. Sagen Sie das fünfmal schnell!

Erstellen

Fangen wir mit den Grundlagen an.

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

Oder ein Upsert.

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

Updates ausführen

Hier sind die Neuigkeiten. Produzenten und Regisseure hoffen natürlich, dass diese durchschnittlichen Bewertungen dem Trend entsprechen.

Das Feld movie_update enthält ein erwartetes id-Argument zur Identifizierung eines Datensatzes und ein data-Feld, mit dem Sie Werte in dieser Aktualisierung festlegen können.

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

Verwenden Sie das Feld movie_updateMany, um mehrere Aktualisierungen durchzuführen.

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

Inkrementieren, dekrementieren, anhängen und vorangestellte Vorgänge mit _update verwenden

Bei _update- und _updateMany-Mutationen können Sie Werte in data: explizit festlegen. Es ist jedoch oft sinnvoller, einen Operator wie „increment“ (incrementieren) anzuwenden, um Werte zu aktualisieren.

Angenommen, Sie möchten die Altersfreigabe eines bestimmten Films erhöhen. Sie können die rating_update-Syntax mit dem inc-Operator verwenden.

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

Data Connect unterstützt die folgenden Operatoren für Feldaktualisierungen:

  • inc, um die Datentypen Int, Int64, Float, Date und Timestamp zu inkrementieren
  • dec zum Dekrementieren der Datentypen Int, Int64, Float, Date und Timestamp

Bei Listen können Sie auch einzelne Werte oder Listen von Werten mithilfe der folgenden Methoden aktualisieren:

  • add, um Elemente an Listentypen anzuhängen, sofern sie noch nicht vorhanden sind(außer Vektorlisten)
  • remove, um alle Elemente (falls vorhanden) aus Listentypen zu entfernen, mit Ausnahme von Vektorlisten
  • append, um Listenelementen Elemente hinzuzufügen, mit Ausnahme von Vektorlisten
  • prepend, um Listentypen Elemente voranzustellen, mit Ausnahme von Vektorlisten

Löschen

Sie können Filmdaten natürlich auch löschen. Filmarchivare möchten die physischen Filme natürlich so lange wie möglich erhalten.

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

Hier können Sie _deleteMany verwenden.

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

Mutationen für Beziehungen schreiben

Hier sehen Sie, wie die implizite _upsert-Mutation auf eine Beziehung angewendet wird.

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

Data Connect-Werte mit der field_expr-Syntax angeben lassen

Wie unter Schlüsselskaläre und Serverwerte erläutert, können Sie Ihr Schema so gestalten, dass der Server Werte für gängige Felder wie ids und Datumsangaben in Beantwortung von Clientanfragen einfügt.

Außerdem können Sie Daten wie Nutzer-IDs verwenden, die in Data Connect request-Objekten aus Client-Apps gesendet werden.

Wenn Sie Mutationen implementieren, verwenden Sie die field_expr-Syntax, um servergenerierte Aktualisierungen auszulösen oder auf Daten aus Anfragen zuzugreifen. Wenn du beispielsweise die in einer Anfrage gespeicherte Autorisierung uid an einen _upsert-Vorgang weitergeben möchtest, gib "auth.uid" in das Feld userId_expr ein.

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

Oder Sie könnten in einer bekannten To-do-Listen-App beim Erstellen einer neuen To-do-Liste id_expr übergeben, um den Server anzuweisen, automatisch eine UUID für die Liste zu generieren.

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

Weitere Informationen finden Sie in der Referenz zu Skalaren unter _Expr.

Mehrere Schritte

Es gibt viele Situationen, in denen Sie mehrere Schreibfelder (z. B. Einfügungen) in einer Mutation angeben möchten. Sie können Ihre Datenbank auch während der Ausführung einer Mutation lesen, um vorhandene Daten abzurufen und zu überprüfen, bevor Sie beispielsweise Einträge oder Aktualisierungen ausführen. Mit diesen Optionen werden Rücksendevorgänge und damit Kosten vermieden.

Mit Data Connect können Sie in Ihren Mutationen mehrstufige Logik anwenden. Dabei werden folgende Funktionen unterstützt:

  • Mehrere Schreibfelder

  • Mehrere Lesefelder in Ihren Mutationen (mit dem Feld-Keyword query)

  • Die @transaction-Anweisung, die Transaktionen unterstützt, wie sie aus relationalen Datenbanken bekannt sind.

  • Die Anweisung @check, mit der Sie den Inhalt von Lesevorgängen mit CEL-Ausdrücken auswerten und basierend auf den Ergebnissen dieser Auswertung Folgendes tun können:

    • Mit der Ausführung von durch eine Mutation definierten Erstellungen, Aktualisierungen und Löschungen fortfahren
    • Ergebnisse eines Abfragefelds zurückgeben
    • Verwende zurückgegebene Nachrichten, um die entsprechende Logik in deinem Clientcode auszuführen.
  • Die Direktive @redact, mit der Sie Ergebnisse von Abfragefeldern aus den Ergebnissen des Wire-Protokolls ausschließen können.

  • Die CEL-response-Bindung, in der die zusammengefassten Ergebnisse aller Mutationen und Abfragen gespeichert werden, die in einem komplexen, mehrstufigen Vorgang ausgeführt wurden. So greifen Sie auf die response-Bindung zu:

    • In @check-Anweisungen über das Argument expr:
    • Mit Serverwerten und field_expr-Syntax

Die @transaction-Anweisung

Die Unterstützung für mehrstufige Mutationen umfasst die Fehlerbehandlung mit Transaktionen.

Die Anweisung @transaction erzwingt, dass eine Mutation – entweder mit einem einzelnen Schreibfeld (z. B. _insert oder _update) oder mit mehreren Schreibfeldern – immer in einer Datenbanktransaktion ausgeführt wird.

  • Bei Mutationen ohne @transaction werden alle Stammfelder nacheinander ausgeführt. Der Vorgang zeigt alle Fehler als Fehler in Teilfeldern an, aber nicht die Auswirkungen der nachfolgenden Ausführungen.

  • Mutationen mit @transaction sind garantiert entweder vollständig erfolgreich oder vollständig fehlgeschlagen. Wenn eines der Felder in der Transaktion fehlschlägt, wird die gesamte Transaktion rückgängig gemacht.

Die Anweisungen @check und @redact

Mit der Anweisung @check wird geprüft, ob die angegebenen Felder in den Abfrageergebnissen vorhanden sind. Feldwerte werden mit einem CEL-Ausdruck (Common Expression Language) getestet. Standardmäßig werden mit der Direktive Knoten geprüft und abgelehnt, deren Wert null oder [] (leere Listen) ist.

Mit der @redact-Richtlinie wird ein Teil der Antwort des Clients entfernt. Ausgeblendete Felder werden weiterhin auf Nebenwirkungen (einschließlich Datenänderungen und @check) geprüft und die Ergebnisse sind für nachfolgende Schritte in CEL-Ausdrücken weiterhin verfügbar.

@check, @check(message:) und @redact verwenden

Eine wichtige Verwendung von @check ad @redact ist das Abrufen zugehöriger Daten, um zu entscheiden, ob bestimmte Vorgänge autorisiert werden sollen. Dabei wird die Suche in der Logik verwendet, aber vor den Clients ausgeblendet. Ihre Abfrage kann nützliche Meldungen für die korrekte Verarbeitung im Clientcode zurückgeben.

Im folgenden Abfragefeld wird beispielsweise geprüft, ob ein Anfragender die Rolle „Administrator“ hat, um Nutzer aufzurufen, die einen Film bearbeiten können.

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

Weitere Informationen zu @check- und @redact-Richtlinien bei Autorisierungsüberprüfungen finden Sie im Abschnitt Suche nach Autorisierungsdaten.

Schlüssel mit @check validieren

Bei einigen Mutationsfeldern wie _update wird möglicherweise keine Aktion ausgeführt, wenn ein Eintrag mit dem angegebenen Schlüssel nicht vorhanden ist. Ebenso können Suchanfragen „null“ oder eine leere Liste zurückgeben. Sie gelten nicht als Fehler und lösen daher keine Rollbacks aus.

Um dies zu vermeiden, testen Sie mit der @check-Anweisung, ob Schlüssel gefunden werden können.

# 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")
}

response-Bindung zum Verketten mehrstufiger Mutationen verwenden

So erstellen Sie zugehörige Einträge, z. B. einen neuen Movie- und einen zugehörige MovieMetadata-Eintrag:

  1. _insert-Mutation für Movie aufrufen
  2. Zurückgegebenen Schlüssel des erstellten Films speichern
  3. Rufen Sie dann eine zweite _insert-Mutation auf, um den MovieMetadata-Eintrag zu erstellen.

Mit Data Connect können Sie diesen häufigen Fall jedoch in einem einzigen mehrstufigen Vorgang bearbeiten, indem Sie im zweiten _insert auf die Ergebnisse des ersten _insert zugreifen.

Eine erfolgreiche App für Filmkritiken zu entwickeln, ist eine Menge Arbeit. Sehen wir uns ein neues Beispiel für unsere To-do-Liste an.

Mit response Felder mit Serverwerten festlegen

Bei der folgenden Änderung an einer To-do-Liste:

  • Die response-Bindung stellt das bisherige Teilantwortobjekt dar, das alle Mutationen auf oberster Ebene vor dem aktuellen enthält.
  • Auf die Ergebnisse des ersten todoList_insert-Vorgangs, der das Feld id (Schlüssel) zurückgibt, wird später in response.todoList_insert.id zugegriffen, damit wir sofort ein neues To-do-Element einfügen können.
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,
  })
}

Felder mit response mit @check validieren

response ist auch in @check(expr: "...") verfügbar, sodass Sie damit noch kompliziertere serverseitige Logik erstellen können. In Kombination mit query { … } Schritten in Mutationen können Sie viel mehr erreichen, ohne zusätzliche Client-zu-Server-Rückgaben.

Beachten Sie im folgenden Beispiel, dass @check bereits Zugriff auf response.query hat, da ein @check immer nach dem Schritt ausgeführt wird, mit dem es verknüpft ist.

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

Weitere Informationen zur response-Bindung finden Sie in der CEL-Referenz.

Unterbrochene Vorgänge mit @transaction und query @check

Bei mehrstufigen Mutationen können Fehler auftreten:

  • Datenbankvorgänge können fehlschlagen.
  • Die Logik der Abfrage @check kann Vorgänge beenden.

Data Connect empfiehlt die Verwendung der @transaction-Direktive für mehrstufige Mutationen. Dies führt zu einheitlicheren Datenbank- und Mutationsergebnissen, die im Clientcode einfacher zu verarbeiten sind:

  • Beim ersten Fehler oder fehlgeschlagenen @check wird der Vorgang beendet. Die Ausführung nachfolgender Felder oder die Auswertung von CEL müssen also nicht verwaltet werden.
  • Rollbacks werden aufgrund von Datenbankfehlern oder @check-Logik ausgeführt, um einen konsistenten Datenbankstatus zu erzielen.
  • Ein Rollback-Fehler wird immer an den Clientcode zurückgegeben.

Es kann Anwendungsfälle geben, in denen Sie @transaction nicht verwenden möchten. Sie können sich beispielsweise für Eventual Consistency entscheiden, wenn Sie beispielsweise einen höheren Durchsatz, eine höhere Skalierbarkeit oder Verfügbarkeit benötigen. Sie müssen jedoch Ihre Datenbank und Ihren Clientcode verwalten, um die Ergebnisse zu ermöglichen:

  • Wenn ein Feld aufgrund von Datenbankvorgängen fehlschlägt, werden die nachfolgenden Felder weiterhin ausgeführt. Fehlgeschlagene @checks beenden jedoch den gesamten Vorgang.
  • Es werden keine Rollbacks ausgeführt. Das bedeutet, dass sich der Datenbankstatus in einem gemischten Zustand befindet, in dem einige Aktualisierungen erfolgreich und andere fehlgeschlagen sind.
  • Ihre Vorgänge mit @check können zu inkonsistenten Ergebnissen führen, wenn die @check-Logik die Ergebnisse von Lese- und/oder Schreibvorgängen in einem vorherigen Schritt verwendet.
  • Das an den Clientcode zurückgegebene Ergebnis enthält eine komplexere Mischung aus Erfolgs- und Fehlerantworten, die verarbeitet werden müssen.

Entsprechendes SQL-Schema

-- 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);

Nächste Schritte