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 自動從架構中的關鍵欄位組合而成的簡潔物件 ID。關鍵標量可提升效率,讓您在單一呼叫中找到資料的識別資訊和結構。當您想對新記錄執行連續動作,並需要傳遞至後續作業的專屬 ID 時,這些鍵就特別實用。此外,如果您想存取關聯鍵來執行其他更複雜的作業,這些鍵也非常實用。

使用伺服器值,您就能讓伺服器根據 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
bit(n)、varbit(n)
char(n)、varchar(n)
整數值 GraphQL int Int2 (smallint、smallserial)、
int4 (整數、int、序列)
浮點值 GraphQL float8 float4 (real)
float8 (double precision)
numeric (十進位)
布林值 GraphQL 布林值 布林值
UUID 自訂 uuid uuid
Int64 自訂 bigint int8 (bigint、bigserial)
數值 (小數)
日期 自訂 date 日期
時間戳記 自訂 timestamptz

timestamptz

注意:系統不會儲存本機時區資訊。
PostgreSQL 會將這類時間戳記轉換並儲存為世界標準時間。

向量 自訂 向量

向量

請參閱「使用 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 指令引數為欄位產生名稱。

查詢和異動的指令

除了用於定義類型和資料表的指令外,Data Connect 還提供 @auth@check@redact@transaction 指令,用於擴充查詢和變異的行為。

指令 適用於 說明
@auth 查詢和異動 定義查詢或 mutation 的授權政策。請參閱授權和認證指南
@check 多步驟作業中的 query 欄位 驗證指定欄位是否出現在查詢結果中。一般運算語言 (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
  }
}

陣列欄位的 includes

您可以測試陣列欄位是否包含指定項目。

# 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 for all fields

最簡單的匯總欄位是 _count:它會傳回與查詢相符的資料列數量。對於類型中的每個欄位,Data Connect 會根據欄位類型產生對應的匯總欄位。

查詢

query CountProducts {
  products {
    _count
  }
}

回覆
one

舉例來說,假設資料庫中有 5 項產品,結果會是:

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

所有欄位都有 <field>_count 欄位,可計算該欄位中含有非空值的資料列數量。

查詢

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 引數可讓您取得欄位 (或欄位組合) 的所有不重複值。例如:

查詢

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 會為每個資料表產生基本 _insert_update 等欄位。

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 倍速唸看看!

建立

讓我們進行基本建立作業。

# 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:在清單類型(向量清單除外) 中置於項目之前

執行刪除作業

當然,你也可以刪除電影資料。電影保存者當然會希望實體電影能盡可能長久保存。

# 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 標量。

多步驟作業

在許多情況下,您可能會想要在單一變異中加入多個寫入欄位 (例如插入)。您可能還想在執行變異時讀取資料庫,以便在執行插入或更新等作業前查詢及驗證現有資料。這些選項可節省往返運算作業,進而節省成本。

Data Connect 可支援以下功能,讓您在突變中執行多步驟邏輯:

  • 多個寫入欄位

  • 在 mutation 中使用多個讀取欄位 (使用 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 ad @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 作業的結果會在稍後的 response.todoList_insert.id 中存取,該作業會傳回 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 的作業可能會產生更多不一致的結果。
  • 傳回至用戶端程式碼的結果會包含更複雜的成功和失敗回應,需要處理。

對等 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);

後續步驟