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

影评数据库查询

您可以使用查询操作类型声明、操作名称、零个或多个操作参数以及零个或多个带参数的指令来定义 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 用例。快速说出这句话五遍!

创建

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

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

后续步骤