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 自动从架构中的键字段组合而成的简洁对象标识符。键标量旨在提高效率,可让您在单次调用中查找有关数据标识和结构的信息。如果您想对新记录执行顺序操作,并且需要传递唯一标识符以供后续操作使用,或者想访问关系键以执行其他更复杂的操作,则这些操作特别有用。

电影元数据表

现在,我们来跟踪电影导演,并与 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 类型 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(双精度)
数值(十进制)
布尔值 GraphQL 布尔值 布尔值
UUID 自定义 uuid uuid
Int64 自定义 bigint int8(bigint、bigserial)
数值(十进制)
日期 自定义 date 日期
时间戳 自定义 timestamptz

timestamptz

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

矢量 自定义 vector

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 指令参数为操作明确设置名称。

查询和更改的 Directive

除了您在定义类型和表时使用的指令之外,Data Connect 还提供了 @auth@check@redact@transaction 指令,用于增强查询和更改的行为。

指令 适用范围 说明
@auth 查询和变更 定义查询或更改的身份验证政策。请参阅授权和认证指南
@check 授权数据查询 验证查询结果中是否包含指定字段。通用表达式语言 (CEL) 表达式用于测试字段值。请参阅授权和认证指南
@redact 查询 从客户端隐去响应的部分内容。请参阅授权和认证指南
@transaction 变更 强制要求更改始终在数据库事务中运行。请参阅电影应用更改示例

对电影评论数据库的查询

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

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

因此,以下查询具有:

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

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

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

查询中的键标量

不过,我们先来了解一下键标量。

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

您可以从大多数隐式更改返回的响应中检索键标量,当然也可以从您检索了构建标量键所需的所有字段的查询中检索键标量。

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

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

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 用例。快速说出这句话五遍!

@transaction 指令

此指令会强制要求更改始终在数据库事务中运行。

使用 @transaction 的更改保证完全成功或完全失败。如果事务中的任何字段都失败,则会回滚整个事务。从客户端的角度来看,任何失败都表现为整个请求因请求错误而失败,并且执行尚未开始。

不带 @transaction 的更改会依次逐个执行每个根字段。它会将所有错误显示为部分字段错误,但不会显示后续执行的影响。

创建

我们来进行一些基本创建操作。

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

授权数据查询

您可以先查询数据库,然后使用 CEL 表达式验证查询结果,以便授权 Data Connect 更新操作。当您向表中写入数据,同时需要检查另一个表中某行的行内容时,这非常有用。

此功能支持:

  • @check 指令,可让您评估字段的内容,并根据此类评估结果执行以下操作:
    • 继续执行由更改定义的创建、更新和删除操作
    • 使用查询向客户端返回的值在客户端中执行不同的逻辑
  • @redact 指令,可让您从线协议结果中省略查询结果。

这些功能对授权流程很有用。

等效的 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);

后续步骤