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") 運算式存取欄位時,定義套用時間戳記的欄位。

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

ID 類型

在 GraphQL 中,ID 類型定義為不透明類型,會序列化為字串。GraphQL 不會限制 ID 格式,但會強制轉型輸入內容中的字串和整數。

PostgreSQL 鍵通常是整數或 UUID,而非字串。Data Connect 會根據結構定義自動產生這類鍵。您可以使用 @default 指令調整金鑰產生作業,如「Actor」表格的 id 欄位定義所示:id: ID! … @default(generate: "UUID")

電影中繼資料表

現在,讓我們追蹤電影導演,並設定與 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 type 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 (decimal)
布林值 GraphQL 布林值 布林值
UUID 自訂 uuid uuid
Int64 自訂 bigint int8 (bigint、bigserial)
numeric (decimal)
日期 自訂 date 日期
時間戳記 自訂 timestamptz

timestamptz

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

列舉 自訂 enum

列舉

向量 自訂 vector

向量

請參閱「使用 Vertex AI 執行向量相似度搜尋」。

  • GraphQL List 會對應至一維陣列。
    • 例如,[Int] 對應至 int5[][Any] 對應至 jsonb[]
    • Data Connect 不支援巢狀陣列。

隱含和預先定義的查詢與異動

您的 Data Connect 查詢和異動會擴充 Data Connect 根據結構定義中的型別和型別關係所產生的隱含查詢隱含異動。每當您編輯結構定義時,本機工具就會產生隱含查詢和突變。

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

隱含查詢和變異命名

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

  • 單一資料表用例的查詢會使用友善名稱 movie (單數,用於擷取傳遞引數 (例如 eq) 的個別結果) 和 movies (複數,用於擷取傳遞引數 (例如 gt) 和作業 (例如 orderby) 的結果清單)。Data Connect 也會為多資料表、關係作業產生查詢,並使用明確名稱,例如 actors_on_moviesactors_via_actormovie
  • 名為 movie_insertmovie_upsert... 的異動

您也可以使用 singularplural 指令引數,明確設定作業名稱。

電影評論資料庫的查詢

您可以使用查詢作業類型宣告、作業名稱、零或多個作業引數,以及零或多個含引數的指令,定義 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
  }
}

陣列欄位的 include

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

# 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 查詢可根據資料表之間的關係存取資料。您可以使用結構定義中定義的物件 (一對一) 或陣列 (一對多) 關係,進行巢狀查詢,也就是擷取某種型別的資料,以及巢狀或相關型別的資料。

這類查詢會在產生的隱含查詢中使用 magic Data Connect _on__via 語法。

您將修改初始版本的結構定義。

多對一

我們來為應用程式新增評論,並修改 User,加入 Review 表格。

# 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 案例。用五倍速唸看看!

建立

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

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

執行更新

以下是最新消息。製片人和導演當然希望這些平均收視率能持續成長。

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

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

後續步驟