Data Connect 架构、查询和变更

Firebase Data Connect 可用于为 PostgreSQL 创建连接器 使用 Google Cloud SQL 管理的实例这些连接器 使用数据的架构、查询和更改。

入门指南介绍了适用于 PostgreSQL 的电影评价应用架构,而本指南将深入探讨如何为 PostgreSQL 设计 Data Connect 架构。

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

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

影评应用的架构

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

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

电影表格

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

  • @table,它允许我们使用 singular 设置操作名称 和 plural 个参数
  • @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")
}

演员兼 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 文本
位(n)、varbit(n)
char(n)、varchar(n)
整数 GraphQL 整数 Int2(smallint、smallserial)、
int4(整数、整数、串行)
浮点数 GraphQL float8 float4(实数)
float8(双精度)
数值(十进制)
布尔值 GraphQL 布尔值 布尔值
UUID 自定义 uuid uuid
Int64 自定义 bigint int8(bigint、bigserial)
数值(十进制)
日期 自定义 日期 日期
时间戳 自定义 timestamptz

timestamptz

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

矢量 自定义 矢量

vector

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

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

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

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

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

隐式查询和更改命名

Data Connect 会根据架构类型声明推断出适合隐式查询和更改的名称。例如,使用 PostgreSQL source,如果您定义名为 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);

后续步骤