Data Connect 架构、查询和变更

Firebase Data Connect 可让您为通过 Google Cloud SQL 管理的 PostgreSQL 实例创建连接器。这些连接器是用于使用数据的架构、查询和变更的组合。

使用入门指南介绍了 PostgreSQL 的电子邮件应用架构,但本指南将更深入地探讨如何为 PostgreSQL 设计 Data Connect 架构,现在使用电影评论数据库作为示例。

本指南将 Data Connect 查询和变更与架构示例配对。为什么要在关于 Data Connect 架构的指南中讨论查询(和变更)?与其他基于 GraphQL 的平台一样,Firebase Data Connect 是一个查询优先的开发平台,因此作为开发者,您在进行数据建模时会考虑客户需要哪些数据,这会极大地影响您为项目开发的数据架构。

本指南首先介绍了一个新的电影评论架构,然后介绍了从该架构派生的查询突变,最后提供了一个与核心 Data Connect 架构等效的 SQL 列表

电影影评应用的架构

假设您想构建一项服务,让用户提交和查看电影评价。

您需要为此类应用提供初始架构。稍后,您将扩展此架构以创建复杂的关系查询。

电影表格

电影的架构包含以下核心指令:

  • @table,可让我们使用 singularplural 实参设置操作名称
  • @col 显式设置列名称
  • @default 以允许设置默认值。
# 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")
}

服务器值和键标量

在介绍电影评价应用之前,我们先来了解一下 Data Connect 服务器值关键标量

借助服务器值,您可以让服务器根据特定的服务器端表达式,使用存储的值或可轻松计算的值动态填充表中的字段。例如,您可以定义一个字段,当使用表达式 updatedAt: Timestamp! @default(expr: "request.time") 访问该字段时,系统会应用时间戳。

关键标量是简洁的对象标识符,可Data Connect自动从架构中的关键字段组装而成。关键标量可提高效率,让您只需一次调用即可找到有关数据身份和结构的信息。当您想对新记录执行一系列操作,并且需要一个唯一标识符来传递给后续操作时,以及当您想访问关系键以执行其他更复杂的操作时,它们尤其有用。

ID 类型

在 GraphQL 中,ID 类型定义为序列化为字符串的不透明类型。GraphQL 不考虑 ID 格式,但会强制转换输入中的字符串和整数。

PostgreSQL 键通常是整数或 UUID,而不是字符串。Data Connect 会根据您的架构自动生成此类键。您可以使用 @default 指令自定义密钥生成,如 Actor 表的 id 字段定义所示:id: ID! … @default(generate: "UUID")

电影元数据表

现在,我们来跟踪电影导演,并设置与 Movie 的一对一关系。

添加 @ref 指令以定义关系。

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

Actor 和 MovieActor

接下来,您希望演员出演您的电影,由于电影和演员之间存在多对多关系,因此请创建一个联接表。

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

用户

最后,是您的应用的用户。

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

支持的数据类型

Data Connect 支持以下标量数据类型,并使用 @col(dataType:) 将其分配给 PostgreSQL 类型。

Data Connect type GraphQL 内置类型或
Data Connect 自定义类型
默认 PostgreSQL 类型 支持的 PostgreSQL 类型
(括号中为别名)
字符串 GraphQL text text
bit(n)、varbit(n)
char(n)、varchar(n)
整数 GraphQL 整数 Int2(smallint、smallserial)、
int4(integer、int、serial)
浮点数 GraphQL float8 float4(实数)
float8(双精度)
numeric(十进制)
布尔值 GraphQL 布尔值 布尔值
UUID 自定义 uuid uuid
Int64 自定义 bigint int8(bigint、bigserial)
numeric(十进制)
日期 自定义 date 日期
时间戳 自定义 timestamptz

timestamptz

注意:系统不会存储本地时区信息。
PostgreSQL 会将此类时间戳转换为世界协调时间 (UTC) 并存储。

枚举 自定义 enum

枚举

向量 自定义 vector

向量

请参阅使用 Vertex AI 执行向量相似度搜索

  • GraphQL List 映射到一维数组。
    • 例如,[Int] 映射到 int5[][Any] 映射到 jsonb[]
    • Data Connect 不支持嵌套数组。

隐式和预定义的查询和变更

您的 Data Connect 查询和变更将扩展 Data Connect 根据架构中的类型和类型关系生成的一组隐式查询隐式变更。每当您修改架构时,本地工具都会生成隐式查询和突变。

在开发过程中,您将根据这些隐式操作实现预定义查询预定义变更

隐式查询和变更命名

Data Connect 会根据您的架构类型声明为隐式查询和突变推断合适的名称。例如,如果使用 PostgreSQL 源,当您定义名为 Movie 的表时,服务器将生成隐式:

  • 对于单表使用情形,使用友好名称 movie(单数,用于检索传递 eq 等实参的单个结果)和 movies(复数,用于检索传递 gt 等实参和 orderby 等操作的结果列表)进行查询。Data Connect 还会生成用于多表、关系型操作的查询,这些查询具有 actors_on_moviesactors_via_actormovie 等明确的名称。
  • 名为 movie_insertmovie_upsert 的突变...

架构定义语言还允许您使用 singularplural 指令实参来明确设置操作名称。

电影评论数据库的查询

您可以使用查询操作类型声明、操作名称、零个或多个操作实参以及零个或多个带实参的指令来定义 Data Connect 查询。

在快速入门中,示例 listEmails 查询不带任何参数。当然,在许多情况下,传递给查询字段的数据是动态的。您可以使用 $variableName 语法将变量用作查询定义的组成部分之一。

因此,以下查询具有:

  • query 类型定义
  • ListMoviesByGenre 操作(查询)名称
  • 单个变量 $genre 操作实参
  • 单个指令 @auth
query ListMoviesByGenre($genre: String!) @auth(level: USER)

每个查询实参都需要一个类型声明,可以是内置类型(如 String),也可以是自定义的架构定义类型(如 Movie)。

我们来看看越来越复杂的查询的签名。最后,我们将介绍隐式查询中提供的强大而简洁的关系表达式,您可以在预定义查询中利用这些表达式。

查询中的关键标量

但首先,请注意一下关键标量。

Data Connect 定义了键标量的特殊类型,由 _Key 标识。例如,对于我们的 Movie 表,键标量的类型为 Movie_Key

您可以从大多数隐式突变返回的响应中检索关键标量,当然也可以从查询中检索,前提是您已检索到构建标量键所需的所有字段。

单个自动查询(例如运行示例中的 movie)支持接受键标量的 key 实参。

您可能会将键标量作为字面量传递。不过,您可以定义变量来传递关键标量作为输入。

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

这些信息可以采用如下所示的 JSON 格式(或其他序列化格式)在请求中提供:

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

借助自定义标量解析,还可以使用对象语法(可包含变量)来构建 Movie_Key。如果您出于某种原因想要将各个组件拆分为不同的变量,此方法会非常有用。

查询中的别名

Data Connect 支持在查询中使用 GraphQL 别名。借助别名,您可以重命名查询结果中返回的数据。单个 Data Connect 查询可以在一个高效的服务器请求中应用多个过滤条件或其他查询操作,从而一次性有效地发出多个“子查询”。为避免返回的数据集中出现名称冲突,您可以使用别名来区分子查询。

以下查询中,表达式使用了别名 mostPopular

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

包含过滤条件的简单查询

Data Connect 查询会映射到所有常见的 SQL 过滤和排序操作。

whereorderBy 运算符(单数查询、复数查询)

返回表(和嵌套关联)中的所有匹配行。如果没有记录与过滤条件匹配,则返回空数组。

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

limitoffset 运算符(单数查询、复数查询)

您可以对结果进行分页。系统会接受这些实参,但不会在结果中返回。

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

数组字段的包含项

您可以测试数组字段是否包含指定项。

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

字符串操作和正则表达式

您的查询可以使用典型的字符串搜索和比较操作,包括正则表达式。请注意,为了提高效率,您在此处捆绑了多个操作,并使用别名消除了歧义。

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

orand 用于组合过滤条件

使用 orand 可实现更复杂的逻辑。

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

复杂的查询

Data Connect 查询可以根据表之间的关系访问数据。您可以使用架构中定义的对象(一对一)或数组(一对多)关系来执行嵌套查询,即提取一种类型的数据以及嵌套或相关类型的数据。

此类查询在生成的隐式查询中使用神奇的 Data Connect _on__via 语法。

您将修改初始版本中的架构。

多对一

我们来为应用添加评价,使用 Review 表并修改 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")
}

查询多对一关系

现在,我们来看一个包含别名的查询,以说明 _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
    }
  }
}

一对一

您可以看到该模式。下方的架构经过修改,仅用于说明。

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

一对一查询

您可以使用 _on_ 语法进行查询。

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

多对多

电影需要演员,演员需要电影。它们之间存在多对多关系,您可以使用 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!]!
}

多对多查询

我们来看一个使用别名的查询,以说明 _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
    }
  }
}

电影评价数据库的变更

如上所述,当您在架构中定义表时,Data Connect 会为每个表生成基本的隐式变更。

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

借助这些功能,您可以实现越来越复杂的 CRUD 核心用例。快速说 5 遍!

创建

我们来执行基本创建操作。

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

或插入/更新。

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

执行更新

以下是最新动态。制片人和导演当然希望这些平均评分能保持上升趋势。

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

执行删除操作

当然,您可以删除电影数据。电影保护主义者肯定希望尽可能长时间地保存实体胶片。

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

您可以在此处使用 _deleteMany

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

对关系写入变更

了解如何在关系上使用隐式 _upsert 变更。

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

等效 SQL 架构

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

接下来怎么做?