實作 Data Connect 異動

Firebase Data Connect 可讓您為透過 Google Cloud SQL 管理的 PostgreSQL 執行個體建立連接器。這些連接器是查詢和突變的組合,可從結構定義使用資料。

入門指南介紹了 PostgreSQL 的電影評論應用程式結構定義。

該指南也介紹了可部署和隨選的管理作業,包括異動。

  • 可部署的變動是指您實作的變動,可從連接器中的用戶端應用程式呼叫,並搭配您定義的 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 欄位進行異動

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

變異的基本要素

資料連結異動是具有 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
  })
}

每個突變引數都需要類型宣告,例如內建的 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: 中的值,但通常套用遞增等運算子來更新值會更有意義。

如要修改先前的更新範例,請假設您想增加特定電影的評分。您可以搭配 inc 運算子使用 rating_update 語法。

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

Data Connect 支援下列欄位更新運算子:

  • inc,以遞增 IntInt64FloatDateTimestamp 資料類型
  • dec,即可遞減 IntInt64FloatDateTimestamp 資料類型

如果是清單,您也可以使用下列項目更新個別值或值清單:

  • add,將項目附加至清單類型(向量清單除外),前提是項目尚未出現在清單中
  • remove 從清單類型中移除所有項目 (向量清單除外)
  • append,將項目附加至清單類型(Vector 清單除外)
  • 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 提供兩項重要功能,可讓您編寫更有效率的突變,並節省往返作業。

主要純量是簡潔的物件 ID,Data Connect 會根據結構定義中的主要欄位自動組裝。主要純量與效率有關,可讓您在單一呼叫中,找到資料的身分和結構相關資訊。當您想對新記錄執行連續動作,且需要將專屬 ID 傳遞至後續作業時,這些方法特別實用。此外,當您想存取關係鍵以執行其他更複雜的作業時,這些方法也很有幫助。

使用伺服器值,您就能有效讓伺服器根據 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 可讓您透過支援下列項目,在變動中執行多步驟邏輯:

  • 多個寫入欄位

  • 變動中的多個讀取欄位 (使用 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) 可能會無效。同樣地,查閱作業可能會傳回空值或空白清單。這些不算是錯誤,因此不會觸發回溯。

為防範這種結果,請測試是否可使用 @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 異動 強制異動一律在資料庫交易中執行。請參閱「多步驟作業」。

後續步驟

你可能感興趣的內容: