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(name)@col(name) 用于自定义 SQL 表和列名称。 如果未指定,Data Connect 会生成 snake_case 名称。
  • @col(dataType) 来自定义 SQL 列类型。
  • @default,用于在插入期间配置 SQL 列默认值。

如需了解详情,请参阅 @table@col@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
}

键标量和服务器值

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

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

使用服务器值,您可以让服务器根据 expr 参数中的特定服务器端 CEL 表达式,使用存储的值或可轻松计算的值有效地动态填充表中的字段。例如,您可以定义一个字段,并在访问该字段时应用时间戳(使用操作请求 updatedAt: Timestamp! @default(expr: "request.time") 中存储的时间)。

电影元数据表

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

添加引用字段以定义关系。

您可以使用 @ref 指令来自定义外键约束条件。

  • @ref(fields) 用于指定外键字段。
  • @ref(references) 来指定目标表中引用的字段。此引用默认为主键,但也支持包含 @unique 的字段。

如需了解详情,请参阅 @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
}

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

用户

最后,是应用的用户。

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

受支持的数据类型

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 字段。每当您修改架构时,这些字段都会由本地工具生成。

  • 正如您在“使用入门”指南中了解到的那样,Firebase 控制台和我们的本地开发工具会使用这些自动生成的字段为您提供临时管理查询和更改,您可以使用这些查询和更改来为表中注入数据并验证表中的内容。

  • 在开发过程中,您将根据这些自动生成的字段,实现连接器中捆绑的可部署查询可部署更改

自动生成的字段命名

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 的修改,向应用添加评价。

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

查询多对一

现在,我们来看看一个使用别名的查询,以说明 _via_ 语法。

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

一对一

您可以看到这种模式。下面,我们修改了架构以作说明。

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

聚合查询

什么是汇总,为什么要使用汇总?

借助汇总字段,您可以对结果列表执行计算。借助汇总字段,您可以执行以下操作:

  • 查找评价的平均分
  • 查找购物车中商品的总费用
  • 查找评分最高或最低的商品
  • 统计商店中的商品数量

汇总是在服务器上执行的,与在客户端计算相比,具有诸多优势:

  • 应用性能更快(因为您避免了客户端计算)
  • 降低数据出站流量费用(因为您只发送汇总结果,而不是所有输入)
  • 增强了安全性(因为您可以向客户授予对汇总数据的访问权限,而不是整个数据集)

汇总的架构示例

在本部分中,我们将改用一个商店示例架构,该架构非常适合用于说明如何使用汇总:

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

简单汇总

所有字段的 _count

最简单的汇总字段是 _count:它会返回与您的查询匹配的行数。对于类型中的每个字段,Data Connect 都会根据字段类型生成相应的汇总字段。

查询

query CountProducts {
  products {
    _count
  }
}

回复
one

例如,如果您的数据库中有 5 件商品,结果将如下所示:

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

所有字段都有一个 <field>_count 字段,用于统计该字段中非 null 值的行数。

查询

query CountProductsWithExpirationDate {
  products {
    expirationDate_count
  }
}

响应
field_count

例如,如果您有 3 件具有有效期的产品,结果将如下所示:

{
  "products": [
    {
    "expirationDate_count": 3
    }
  ]
}
适用于数字字段的 _min、_max、_sum 和 _avg

数值字段(int、float、int64)也具有 <field>_min<field>_max<field>_sum<field>_avg

查询

query NumericAggregates {
  products {
  quantityInStock_max
  price_min
  price_avg
  quantityInStock_sum
  }
}

响应
_min _max _sum _avg

例如,如果您有以下商品:

  • 产品 A:quantityInStock: 10price: 2.99
  • 商品 B:quantityInStock: 5price: 5.99
  • 商品 C:quantityInStock: 20price: 1.99

结果如下:

{
  "products": [
    {
    "quantityInStock_max": 20,
    "price_min": 1.99,
    "price_avg": 3.6566666666666666,
    "quantityInStock_sum": 35
    }
  ]
}
日期和时间戳的 _min 和 _max

日期和时间戳字段包含 <field>_min<field>_max

查询

query DateAndTimeAggregates {
  products {
  expirationDate_max
  expirationDate_min
  }
}

响应
_min _maxdatetime

例如,如果您有以下到期日期:

  • 商品 A:2024-01-01
  • 产品 B:2024-03-01
  • 商品 C:2024-02-01

结果如下:

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

Distinct

借助 distinct 参数,您可以获取字段(或字段组合)的所有唯一值。例如:

查询

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

响应
distinct

例如,如果您有以下制造商:

  • 商品 A:manufacturer: "Acme"
  • 产品 B:manufacturer: "Beta"
  • 商品 C:manufacturer: "Acme"

结果如下:

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

您还可以对汇总字段使用 distinct 参数,以汇总不重复的值。例如:

查询

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

响应
distinctonaggregate

例如,如果您有以下制造商:

  • 商品 A:manufacturer: "Acme"
  • 产品 B:manufacturer: "Beta"
  • 商品 C:manufacturer: "Acme"

结果如下:

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

分组汇总

您可以通过对某个类型选择混合聚合字段和非聚合字段来执行分组汇总。这会将所有具有相同非汇总字段值的匹配行归为一组,并计算该组的汇总字段。例如:

查询

query MostExpensiveProductByManufacturer {
  products {
  manufacturer
  price_max
  }
}

响应
groupedaggregates

例如,如果您有以下商品:

  • 产品 A:manufacturer: "Acme"price: 2.99
  • 商品 B:manufacturer: "Beta"price: 5.99
  • 商品 C:manufacturer: "Acme"price: 1.99

结果如下:

{
  "products": [
    { "manufacturer": "Acme", "price_max": 2.99 },
    { "manufacturer": "Beta", "price_max": 5.99 }
  ]
}
havingwhere 包含分组汇总

您还可以使用 havingwhere 参数,以便仅返回符合所提供条件的组。

  • 借助 having,您可以按汇总字段过滤组
  • 借助 where,您可以根据非汇总字段过滤行。

查询

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

响应
havingwhere

例如,如果您有以下商品:

  • 产品 A:manufacturer: "Acme"price: 2.99
  • 商品 B:manufacturer: "Beta"price: 5.99
  • 商品 C:manufacturer: "Acme"price: 1.99

结果如下:

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

跨表汇总

汇总字段可与生成的一对多关系字段搭配使用,以解答有关数据的复杂问题。下面是一个经过修改的表架构,其中包含单独的表 Manufacturer,我们可以在示例中使用它:

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

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

现在,我们可以使用汇总字段执行一些操作,例如查找制造商生产的产品数量:

查询

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

回复
aggregatesacrosstables

例如,如果您有以下制造商:

  • 制造商 A:name: "Acme"products_on_manufacturer: 2
  • 制造商 B:name: "Beta"products_on_manufacturer: 1

结果如下:

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

影评数据库的更改

如前所述,当您在架构中定义表时,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 遍!

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

执行更新

以下是最新动态。制片人和导演当然希望这些平均收视率符合趋势。

movie_update 字段包含用于标识记录的预期 id 参数,以及一个 data 字段,您可以使用该字段在此更新中设置值。

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

如需执行多个更新,请使用 movie_updateMany 字段。

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

使用 _update 执行增量、减量、附加和前置操作

虽然在 _update_updateMany 更改中,您可以在 data: 中明确设置值,但通常更明智的做法是应用增量等运算符来更新值。

如需修改前面的更新示例,假设您想提高特定电影的评分。您可以将 rating_update 语法与 inc 运算符搭配使用。

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

Data Connect 支持以下字段更新运算符:

  • inc 用于递增 IntInt64Float 数据类型
  • dec 用于递减 IntInt64Float 数据类型
  • append,用于附加到列表类型(矢量列表除外)
  • prepend,用于在列表类型(矢量列表除外)前面附加

执行删除操作

当然,您可以删除电影数据。电影修复人员当然希望尽可能长时间保留实体电影。

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

Data Connect 使用 field_expr 语法提供值

键标量值和服务器值中所述,您可以设计架构,以便服务器在响应客户端请求时为 id 和日期等常见字段填充值。

此外,您还可以使用从客户端应用发送的 Data Connect request 对象中发送的数据(例如用户 ID)。

实现更改时,请使用 field_expr 语法触发服务器生成的更新或访问请求中的数据。例如,如需将请求中存储的授权 uid 传递给 _upsert 操作,请在 userId_expr 字段中传递 "auth.uid"

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

或者,在熟悉的待办事项应用中,创建新待办事项时,您可以传递 id_expr 来指示服务器为该列表自动生成 UUID。

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

如需了解详情,请参阅标量参考文档中的 _Expr 标量。

授权数据查询

您可以先查询数据库,然后使用 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);

后续步骤