实现 SQL Connect mutation

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

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

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

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

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

变更的功能SQL Connect

SQL Connect 借助 SQL Connect,您可以按照 PostgreSQL 数据库的预期方式执行基本变更:

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

但是,借助 SQL Connect 对 GraphQL 的扩展,您可以实现 高级变更,从而打造速度更快、效率更高的应用:

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

使用生成的字段实现变更

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

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

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

使用
movie_insert 字段进行变更

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

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

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

使用
movie_update 字段进行变更

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

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

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

使用
movie_delete 字段进行变更

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

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

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

变更的基本要素

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

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

每个变更参数都需要类型声明、内置类型(例如 String)或自定义的架构定义类型(例如 Movie)。

编写基本变更

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

创建

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

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

或者执行 upsert 操作。

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

SQL Connect 支持以下运算符进行字段更新:

  • inc,用于递增 IntInt64FloatDateTimestamp 数据类型
  • dec,用于递减 IntInt64FloatDateTimestamp 数据类型

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

  • add,用于将项附加到列表类型(如果这些项尚未存在于列表中),但矢量列表除外
  • remove,用于从列表类型中移除所有项(如果存在),但矢量列表除外
  • 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 }
  )
}

设计架构以实现高效变更

SQL Connect 提供了两个重要功能,可让您 编写更高效的变更并节省往返操作。

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

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

编写高级变更:让 SQL Connect 使用 field_expr 语法提供值

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

此外,您还可以使用从客户端应用中的 SQL 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标量参考中的 标量

编写高级变更:多步骤操作

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

SQL Connect 支持以下功能,可让您在变更中执行多步骤逻辑:

  • 多个写入字段

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

  • @transaction 指令,它提供 关系型数据库中熟悉的事务支持。

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

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

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

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

@transaction 指令

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

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

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

  • 带有 @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 记录。

但是,借助 SQL 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 中断的操作

多步骤变更可能会遇到错误:

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

SQL Connect 建议您将 @transaction 指令与 多步骤变更一起使用。这样可以获得更一致的数据库和变更结果,以便在客户端代码中更轻松地处理:

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

在某些用例中,您可以选择不使用 @transaction:例如,如果您需要更高的吞吐量、可伸缩性或可用性,则可以选择最终一致性。但是,您需要管理数据库和客户端代码以允许结果:

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

SQL Connect 变更的指令

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

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

后续步骤

您可能感兴趣的内容: