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,可讓我們使用 singularplural 引數設定作業名稱
  • @col 可明確設定資料欄名稱
  • @default 允許設定預設值。
# 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")
}

伺服器值和主要標度

在查看電影評論應用程式之前,讓我們先介紹 Data Connect 伺服器值鍵向量

使用伺服器值,您就能根據特定伺服器端運算式,讓伺服器根據儲存或可輕鬆計算的值,動態填入資料表中的欄位。舉例來說,您可以使用 updatedAt: Timestamp! @default(expr: "request.time") 運算式定義欄位,並在使用者存取該欄位時套用時間戳記。

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

電影中繼資料表

接下來,我們來追蹤電影導演,並設定與 Movie 的一對一關係。

新增 @ref 指令來定義關係。

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

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(name: "Actors", singular: "actor", plural: "actors") {
  id: UUID! @col(name: "actor_id") @default(expr: "uuidV4()")
  name: String! @col(name: "name", dataType: "varchar(30)")
}
# Join table for many-to-many relationship for movies and actors
# The 'key' param signifies the primary key(s) of this table
# In this case, the keys are [movieId, actorId], the generated fields of the reference types [movie, actor]
type MovieActor @table(key: ["movie", "actor"]) {
  # @ref creates a field in the current table (MovieActor) 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, see: implicit.gql
  actor: Actor! @ref
  # actorId: UUID! <- this is created by the above @ref, see: implicit.gql
  role: String! @col(name: "role") # "main" or "supporting"
  # optional other fields
}

使用者

最後,是應用程式的使用者。

# Users
# Suppose a user can leave reviews for movies
# user:reviews is a one to many relationship, movie:reviews is a one to many relationship, movie:user is a many to many relationship
type User
  @table(name: "Users", singular: "user", plural: "users", key: ["id"]) {
  id: UUID! @col(name: "user_id") @default(expr: "uuidV4()")
  auth: String @col(name: "user_auth") @default(expr: "auth.uid")
  username: String! @col(name: "username", dataType: "varchar(30)")
  # The following are generated from the @ref in the Review table
  # reviews_on_user
  # movies_via_Review
}

支援的資料類型

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 (integer、int、serial)
浮點值 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 根據結構定義中的類型和類型關係產生的一組隱含查詢隱含變異。每當您編輯結構定義時,本機工具就會產生隱含查詢和變異。

在開發過程中,您將根據這些隱含作業實作預先定義的查詢預先定義的變異

隱含查詢和變異名稱

Data Connect 會根據結構定義類型宣告,推斷內隱查詢和變異的適當名稱。舉例來說,如果您使用 PostgreSQL 來源,並定義名為 Movie 的資料表,伺服器會產生隱含的:

  • 針對單一資料表使用情境的查詢,其名稱為 movie (單數,用於擷取傳遞 eq 等引數的個別結果,以及 orderby 等作業) 和 movies (複數,用於擷取傳遞 gt 等引數的結果清單,以及 orderby 等作業)。Data Connect 也會針對多資料表、關聯式作業產生查詢,其名稱為 actors_on_moviesactors_via_actormovie
  • 名為 movie_insertmovie_upsert 的異動

您也可以使用結構定義語言,透過 singularplural 指示引數,明確設定作業名稱。

查詢和突變的指令

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

指令 適用於 說明
@auth 查詢和異動 定義查詢或 mutation 的驗證政策。請參閱授權和認證指南
@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
  }
}

陣列欄位的 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 的修改內容。

# Users
# Suppose a user can leave reviews for movies
# user:reviews is a one to many relationship,
# movie:reviews is a one to many relationship,
# movie:user is a many to many relationship
type User
  @table(name: "Users", singular: "user", plural: "users", key: ["id"]) {
  id: UUID! @col(name: "user_id") @default(expr: "uuidV4()")
  auth: String @col(name: "user_auth") @default(expr: "auth.uid")
  username: String! @col(name: "username", dataType: "varchar(30)")
  # The following are generated from the @ref in the Review table
  # reviews_on_user
  # movies_via_Review
}
# Reviews
type Review @table(name: "Reviews", key: ["movie", "user"]) {
  id: UUID! @col(name: "review_id") @default(expr: "uuidV4()")
  user: User! @ref
  movie: Movie! @ref
  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
      description
    }
    dislikedMovies: movies_via_review(where: { rating: { le: 2 } }) {
      title
      genre
      description
    }
  }
}

一對一

您可以看到模式。以下為示範目的修改結構定義。

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

電影評論資料庫的異動

如前所述,在結構定義中定義資料表時,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"
  })
}

執行更新

以下是最新消息。製作人和導演當然希望這些平均評分能維持在趨勢上。

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

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

執行刪除作業

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

# 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 變異體可透過先查詢資料庫,然後使用 CEL 運算式驗證查詢結果的方式授權。當您寫入資料表,並需要檢查其他資料表中資料列的內容時,這項功能就非常實用。

這項功能支援:

  • @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);

後續步驟