实现 Data Connect 更改

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

使用入门指南介绍了 PostgreSQL 的电影评论应用架构。

该指南还介绍了可部署的管理操作和临时管理操作,包括变更。

  • 可部署的 mutation 是指您在连接器中实现以从客户端应用调用的 mutation,具有您定义的 API 端点。Data Connect 将身份验证和授权集成到这些突变中,并根据您的 API 生成客户端 SDK。
  • 临时管理突变从特权环境中运行,用于填充和管理表。您可以在 Firebase 控制台中创建并执行这些查询,也可以从使用 Firebase Admin SDK 的特权环境中执行,还可以在使用 Data Connect VS Code 扩展程序的本地开发环境中执行。

本指南将深入探讨可部署的突变

Data Connect 突变的特征

Data Connect 可让您以预期的方式在 PostgreSQL 数据库中执行基本突变:

  • 执行 CRUD 操作
  • 使用事务管理多步操作

不过,借助 Data Connect 对 GraphQL 的扩展,您可以实现高级变更,从而打造更快、更高效的应用:

  • 使用许多操作返回的键标量来简化对记录的重复操作
  • 使用服务器值通过服务器提供的操作填充数据
  • 在多步突变操作过程中执行查询以查找数据,从而节省代码行数并减少与服务器的往返次数。

使用生成的字段来实现突变

您的 Data Connect 操作将扩展一组根据架构中的类型和类型关系自动生成的 Data Connect。每当您修改架构时,本地工具都会生成这些字段。

您可以使用生成的字段来实现各种突变,从在单个表中创建、更新和删除单个记录,到更复杂的多表更新。

假设您的架构包含 Movie 类型和关联的 Actor 类型。Data Connect 会生成 movie_insertmovie_updatemovie_delete 字段等。

包含
movie_insert 字段的 mutation

movie_insert 字段表示在 Movie 表中创建单个记录的变更。

使用此字段可创建单个电影。

mutation CreateMovie($data: Movie_Data!) {
  movie_insert(data: $data) { key }
}

包含
movie_update 字段的 mutation

movie_update 字段表示用于更新 Movie 表中单个记录的变更。

使用此字段按键更新单个电影。

mutation UpdateMovie($myKey: Movie_Key!, $data: Movie_Data!) {
  movie_update(key: $myKey, data: $data) { key }
}

包含
movie_delete 字段的 mutation

movie_delete 字段表示用于删除 Movie 表中单个记录的变更。

使用此字段按键删除单个电影。

  mutation DeleteMovie($myKey: Movie_Key!) {
    movie_delete(key: $myKey) { key }
  }

变更的基本要素

Data Connect 变更是指具有 Data Connect 扩展功能的 GraphQL 变更。与常规 GraphQL 变更一样,您可以定义操作名称和 GraphQL 变量列表。

Data Connect 通过自定义指令(例如 @auth@transaction)扩展了 GraphQL 查询。

因此,以下变异具有:

  • mutation 类型定义
  • SignUp 操作(变更)名称
  • 单个变量 $username 操作实参
  • 单个指令,即 @auth
  • 单个字段 user_insert
mutation SignUp($username: String!) @auth(level: USER) {
  user_insert(data: {
    id_expr: "auth.uid"
    username: $username
  })
}

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

编写基本变更

您可以开始编写 mutation,以创建、更新和删除数据库中的各个记录。

创建

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

# 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,用于递增 IntInt64FloatDateTimestamp 数据类型
  • dec 用于递减 IntInt64FloatDateTimestamp 数据类型

对于列表,您还可以使用以下方法更新单个值或值列表:

  • add 用于将项(如果尚未存在)附加到列表类型(矢量列表除外)
  • remove 用于从列表类型(矢量列表除外)中移除所有项(如果存在)
  • append 用于将一个或多个项附加到列表类型(矢量列表除外)
  • prepend 用于将一个或多个项添加到列表类型(Vector 列表除外)的前面

执行删除操作

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

# 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 提供了两项重要功能,可让您编写更高效的更改并节省往返操作。

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

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

编写高级变更:让 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 标量。

编写高级突变:多步操作

在许多情况下,您可能需要在一次变更中包含多个写入字段(例如插入)。您可能还希望在执行突变期间读取数据库,以便在执行插入或更新等操作之前查找并验证现有数据。这些选项可节省往返操作,从而节省费用。

借助 Data Connect,您可以通过以下方式在 mutation 中执行多步逻辑:

  • 多个写入字段

  • 变异中的多个读取字段(使用 query 字段关键字)。

  • @transaction 指令,可提供关系型数据库中常见的事务支持。

  • @check 指令,可让您使用 CEL 表达式评估读取的内容,并根据评估结果:

    • 继续执行由变更定义的创建、更新和删除操作
    • 继续返回查询字段的结果
    • 使用返回的消息在客户端代码中执行适当的逻辑
  • @redact 指令,可让您从有线协议结果中省略查询字段结果。

  • CEL response 绑定,用于存储在复杂的多步骤操作中执行的所有更改和查询的累积结果。您可以访问 response 绑定:

    • @check 指令中,通过 expr: 实参
    • 使用服务器值(使用 field_expr 语法)

@transaction 指令

对多步突变的支持包括使用事务进行错误处理。

@transaction 指令强制规定,无论是具有单个写入字段(例如 _insert_update)还是具有多个写入字段的变更,都必须在数据库事务中运行。

  • 不含 @transaction 的 mutation 会按顺序依次执行每个根字段。该操作会将所有错误显示为部分字段错误,但不会显示后续执行的影响。

  • 带有 @transaction 的突变保证完全成功或完全失败。如果事务中的任何字段失败,则整个事务都会回滚。

@check@redact 指令

@check 指令用于验证查询结果中是否存在指定字段。通用表达式语言 (CEL) 表达式用于测试字段值。该指令的默认行为是检查并拒绝值为 null[](空列表)的节点。

@redact 指令会从客户端的响应中编辑一部分内容。系统仍会评估已遮盖字段的副作用(包括数据更改和 @check),并且结果仍可在 CEL 表达式中用于后续步骤。

使用 @check@check(message:)@redact

@check@redact 的主要用途是查找相关数据,以决定是否应授权执行某些操作,在逻辑中使用查找功能,但对客户端隐藏该功能。您的查询可以返回有用的消息,以便在客户端代码中正确处理。

为便于说明,以下查询字段会检查请求者是否具有适当的“管理员”角色来查看可以修改电影的用户。

query GetMovieEditors($movieId: UUID!) @auth(level: USER) {
  moviePermission(key: { movieId: $movieId, userId_expr: "auth.uid" }) @redact {
    role @check(expr: "this == 'admin'", message: "You must be an admin to view all editors of a movie.")
  }
  moviePermissions(where: { movieId: { eq: $movieId }, role: { eq: "editor" } }) {
    user {
      id
      username
    }
  }
}

如需详细了解授权检查中的 @check@redact 指令,请参阅有关授权数据查找的讨论

使用 @check 验证密钥

如果具有指定键的记录不存在,某些突变字段(例如 _update)可能会无操作。同样,查找可能会返回 null 或空列表。这些不被视为错误,因此不会触发回滚。

为防范此结果,请使用 @check 指令测试是否可以找到密钥。

# Delete by key, error if not found
mutation MustDeleteMovie($id: UUID!) @transaction {
  movie_delete(id: $id) @check(expr: "this != null", message: "Movie not found, therefore nothing is deleted")
}

使用 response 绑定来链接多步突变

创建相关记录(例如新的 Movie 和关联的 MovieMetadata 条目)的基本方法是:

  1. Movie 调用 _insert 突变
  2. 存储所创建电影的返回键
  3. 然后,调用第二个 _insert 突变来创建 MovieMetadata 记录。

不过,借助 Data Connect,您可以通过在第二个 _insert 中访问第一个 _insert结果,以单个多步操作处理这种常见情况。

开发一款成功的影评应用需要付出大量努力。我们通过一个新示例来跟踪待办事项列表。

使用 response 设置具有服务器值的字段

在以下待办事项列表突变中:

  • response 绑定表示目前为止的部分响应对象,其中包含当前突变之前的所有顶级突变字段。
  • 初始 todoList_insert 操作的结果(返回 id [键] 字段)稍后会在 response.todoList_insert.id 中访问,以便我们立即插入新的待办事项。
mutation CreateTodoListWithFirstItem(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1:
  todoList_insert(data: {
    id_expr: "uuidV4()", # <-- auto-generated. Or a column-level @default on `type TodoList` will also work
    name: $listName,
  })
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoList_insert.id" # <-- Grab the newly generated ID from the partial response so far.
    content: $itemContent,
  })
}

使用 response 通过 @check 验证字段

response 也适用于 @check(expr: "..."),因此您可以使用它来构建更复杂的服务器端逻辑。与突变中的 query { … } 步数相结合,您无需任何额外的客户端-服务器往返即可实现更多功能。

在以下示例中,请注意:@check 已经有权访问 response.query,因为 @check 始终在所附加的步骤之后运行。

mutation CreateTodoInNamedList(
  $listName: String!,
  $itemContent: String!
) @transaction {
  # Sub-step 1: Look up List.id by its name
  query
  @check(expr: "response.query.todoLists.size() > 0", message: "No such TodoList with the name!")
  @check(expr: "response.query.todoLists.size() < 2", message: "Ambiguous listName!") {
    todoLists(where: { name: $listName }) {
      id
    }
  }
  # Sub-step 2:
  todo_insert(data: {
    listId_expr: "response.todoLists[0].id" # <-- Now we have the parent list ID to insert to
    content: $itemContent,
  })
}

如需详细了解 response 绑定,请参阅 CEL 参考文档

通过 @transactionquery @check 了解中断的操作

多步突变可能会遇到错误:

  • 数据库操作可能会失败。
  • 查询 @check 逻辑可能会终止操作。

Data Connect 建议您将 @transaction 指令与多步突变搭配使用。这样可确保数据库更加一致,并让客户端代码更轻松地处理突变结果:

  • 在出现第一个错误或 @check 失败时,操作将终止,因此无需管理任何后续字段的执行或 CEL 的评估。
  • 回滚是为响应数据库错误或 @check 逻辑而执行的,可确保数据库状态一致。
  • 系统始终会向客户端代码返回回滚错误。

在某些使用情形下,您可能会选择不使用 @transaction:例如,如果您需要更高的吞吐量、可伸缩性或可用性,则可以选择最终一致性。不过,您需要管理数据库和客户端代码,以便获得结果:

  • 如果某个字段因数据库操作而失败,后续字段将继续执行。不过,失败的 @check 仍会终止整个操作。
  • 不执行回滚,这意味着数据库处于混合状态,其中一些更新成功,另一些更新失败。
  • 如果您的 @check 逻辑使用上一步骤中的读取和/或写入结果,则使用 @check 的操作可能会产生更多不一致的结果。
  • 返回给客户端代码的结果将包含更复杂的成功和失败响应组合,需要进行处理。

针对 Data Connect 变更的指令

除了在定义类型和表时使用的指令之外,Data Connect 还提供了 @auth@check@redact@transaction 指令来增强操作的行为。

指令 适用范围 说明
@auth 查询和变更 定义查询或突变的授权政策。请参阅授权和证明指南
@check 多步操作中的 query 字段 验证指定字段是否出现在查询结果中。使用通用表达式语言 (CEL) 表达式来测试字段值。请参阅多步操作
@redact 查询 隐去客户端响应中的一部分内容。请参阅多步操作
@transaction 变更 强制执行变更始终在数据库事务中运行。请参阅多步操作

后续步骤

您可能感兴趣的内容: