Firebase Data Connect 可讓您為透過 Google Cloud SQL 管理的 PostgreSQL 執行個體建立連接器。這些連接器是結構定義、查詢和變動的組合,可用於資料。
入門指南介紹了 PostgreSQL 的電子郵件應用程式結構定義,但本指南會深入探討如何為 PostgreSQL 設計 Data Connect 結構定義,現在使用電影評論資料庫做為動機範例。
本指南會搭配結構定義範例,說明 Data Connect 查詢和異動。為什麼要在結構定義指南中討論查詢 (和異動)?Data Connect與其他以 GraphQL 為基礎的平台一樣,Firebase Data Connect 是以查詢為優先的開發平台,因此開發人員在進行資料建模時,會考量用戶端需要的資料,這將大幅影響專案的資料結構定義。
本指南會先介紹電影評論的新結構定義,然後說明從該結構定義衍生的查詢和突變,最後提供與核心 Data Connect 結構定義對應的 SQL 清單。
電影評論應用程式的結構定義
假設您要建構一項服務,讓使用者提交及查看電影評論。
這類應用程式需要初始結構定義,您稍後會擴充這個結構定義,建立複雜的關聯式查詢。
電影資料表
電影的結構定義包含下列核心指令:
@table,可讓我們使用singular和plural引數設定作業名稱@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 注意:系統不會儲存當地時區資訊。 |
| 列舉 | 自訂 | 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_movies或actors_via_actormovie。 - 名為
movie_insert、movie_upsert... 的異動
您也可以使用 singular 和 plural 指令引數,明確設定作業名稱。
電影評論資料庫的查詢
您可以使用查詢作業類型宣告、作業名稱、零或多個作業引數,以及零或多個含引數的指令,定義 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 篩選和排序作業。
where 和 orderBy 運算子 (單數和複數查詢)
傳回資料表 (和巢狀關聯) 中所有相符的資料列。如果沒有任何記錄符合篩選條件,則傳回空陣列。
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}]) { … }
}
limit 和 offset 運算子 (單數和複數查詢)
您可以對結果執行分頁。系統會接受這些引數,但不會在結果中傳回。
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}}}) {...}
}
or 和 and,用於組合篩選器
如需更複雜的邏輯,請使用 or 和 and。
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);
後續步驟
- 瞭解如何從自動產生的網頁 SDK、Android SDK、iOS SDK 和 Flutter SDK 呼叫查詢和變動。