Firebase Data Connect 提供多種與 Cloud SQL 資料庫互動的方式:
- 原生 GraphQL:在
schema.gql中定義型別,並將 GraphQL 作業轉換為 SQL。Data Connect這是標準做法,可提供嚴格型別和結構定義強制執行的結構。本頁面以外的Data Connect說明文件大多會討論這個選項。建議盡可能使用這個方法,充分發揮型別安全和工具支援的優勢。 @view指令:在schema.gql中定義 GraphQL 型別,並以自訂SELECTSQL 陳述式做為後端。這項功能有助於根據複雜的 SQL 邏輯,建立唯讀的強型別檢視區塊。這些型別可像一般型別一樣查詢。請參閱「@view」。- 原生 SQL:直接在 . 中具名作業中嵌入 SQL 陳述式。
gql檔案,方法是使用特殊的根欄位。這項功能可提供最大彈性和直接控制權,特別是對於標準 GraphQL 中不易表達的作業、運用資料庫專屬功能或使用 PostgreSQL 擴充功能時。
本指南著重於「原生 SQL」選項。
原生 SQL 的常見用途
雖然原生 GraphQL 提供完整的型別安全,且 @view 指令可為唯讀 SQL 報表提供強型別結果,但原生 SQL 仍具備下列彈性:
- PostgreSQL 擴充功能:直接查詢及使用任何已安裝的 PostgreSQL 擴充功能 (例如地理空間資料的
PostGIS),不必在 GraphQL 結構定義中對應複雜型別。 - 複雜查詢:執行複雜的 SQL,包括聯結、子查詢、彙整、視窗函式和預存程序。
- 資料操縱 (DML):直接執行
INSERT, UPDATE, DELETE作業。(但請勿使用原生 SQL 執行資料定義語言 (DDL) 指令。您必須繼續使用 GraphQL 進行結構定義層級的變更,確保後端和產生的 SDK 保持同步。) - 資料庫專屬功能:使用 PostgreSQL 專屬的函式、運算子或資料型別。
- 效能最佳化:針對重要路徑手動調整 SQL 陳述式。
原生 SQL 根欄位
如要使用 SQL 撰寫作業,請使用 query 或 mutation 類型的下列其中一個根欄位:
query 個欄位
| 欄位 | 說明 |
|---|---|
_select |
執行 SQL 查詢,傳回零或多個資料列。 引數:
傳回:JSON 陣列 ( |
_selectFirst |
執行預期會傳回零或一個資料列的 SQL 查詢。 引數:
傳回:JSON 物件 ( |
mutation 個欄位
| 欄位 | 說明 |
|---|---|
_execute |
執行 DML 陳述式 ( 引數:
傳回: 結果會忽略 |
_executeReturning |
執行含有 引數:
傳回:JSON 陣列 ( |
_executeReturningFirst |
執行含有 引數:
傳回:JSON 物件 ( |
注意:
系統會使用授予 Data Connect 服務帳戶的權限執行作業。
如果您使用
@table指令 (@table(name: "ExampleTable")) 明確設定資料表名稱,也必須在 SQL 陳述式中將資料表名稱加上引號 (SELECT field FROM "ExampleTable" ...)。如果沒有引號,Data Connect 會將資料表名稱轉換為蛇形命名法 (
example_table)。
語法規則與限制
原生 SQL 會強制執行嚴格的剖析規則,確保安全性並防止 SQL 植入。請注意下列限制:
- 註解:請使用區塊註解 (
/* ... */)。禁止使用行註解 (--),因為在查詢串連期間,這類註解可能會截斷後續子句 (例如安全性篩選器)。 - 參數:使用與
params陣列順序相符的位置參數 ($1、$2)。不支援具名參數 ($id、:name)。 - 字串:支援擴充字串常值 (
E'...') 和以 $ 引號括住的字串 ($$...$$)。系統不支援 PostgreSQL Unicode 逸出字元 (U&'...')。
留言中的參數
剖析器會忽略區塊註解中的所有內容。如果註解掉含有參數的行 (例如 /* WHERE id = $1 */),您也必須從 params 清單中移除該參數,否則作業會失敗並顯示 unused parameter: $1 錯誤。
範例
範例 1:使用欄位別名的基本 SELECT
您可以為根欄位設定別名 (例如 movies: _select),讓用戶端回應更簡潔 (data.movies 而非 data._select)。
queries.gql:
query GetMoviesByGenre($genre: String!, $limit:Int!) @auth(level: PUBLIC) {
movies: _select(
sql: """
SELECT id, title, release_year, rating
FROM movie
WHERE genre = $1
ORDER BY release_year DESC
LIMIT $2
""",
params: [$genre, $limit]
)
}
使用用戶端 SDK 執行查詢後,結果會顯示在 data.movies 中。
範例 2:基本 UPDATE
mutations.gql:
mutation UpdateMovieRating($movieId: UUID!, $newRating: Float!) @auth(level: NO_ACCESS) {
_execute(
sql: """
UPDATE movie
SET rating = $2
WHERE id = $1
""",
params: [$movieId, $newRating]
)
}
使用用戶端 SDK 執行變動後,受影響的資料列數量會顯示在 data._execute 中。
範例 3:基本匯總
queries.gql:
query GetTotalReviewCount @auth(level: PUBLIC) {
stats: _selectFirst(
sql: "SELECT COUNT(*) as total_reviews FROM \"Reviews\""
)
}
使用用戶端 SDK 執行查詢後,結果會顯示在 data.stats.total_reviews 中。
範例 4:使用 RANK 進行進階匯總
queries.gql:
query GetMoviesRankedByRating @auth(level: PUBLIC) {
_select(
sql: """
SELECT
id,
title,
rating,
RANK() OVER (ORDER BY rating DESC) as rank
FROM movie
WHERE rating IS NOT NULL
LIMIT 20
""",
params: []
)
}
使用用戶端 SDK 執行查詢後,結果會顯示在 data._select 中。
範例 5:使用 RETURNING 和 Auth Context 進行 UPDATE
mutations.gql:
mutation UpdateMyReviewText($movieId: UUID!, $newText: String!) @auth(level: USER) {
updatedReview: _executeReturningFirst(
sql: """
UPDATE "Reviews"
SET review_text = $2
WHERE movie_id = $1 AND user_id = $3
RETURNING movie_id, user_id, rating, review_text
""",
params: [$movieId,$newText,{_expr: "auth.uid" }]
)
}
使用用戶端 SDK 執行變異後,更新後的貼文資料會位於 data.updatedReview 中。
範例 6:使用 upsert 的進階 CTE (不可分割的 get-or-create)
這個模式有助於確保在插入子項記錄 (例如評論) 之前,存在相依的記錄 (例如使用者或電影),而且所有作業都在單一資料庫交易中完成。
mutations.gql:
mutation CreateMovieCTE($movieId: UUID!, $userId: UUID!, $reviewId: UUID!) {
_execute(
sql: """
WITH
new_user AS (
INSERT INTO "user" (id, username)
VALUES ($2, 'Auto-Generated User')
ON CONFLICT (id) DO NOTHING
RETURNING id
),
movie AS (
INSERT INTO movie (id, title, image_url, release_year, genre)
VALUES ($1, 'Auto-Generated Movie', 'https://placeholder.com', 2025, 'Sci-Fi')
ON CONFLICT (id) DO NOTHING
RETURNING id
)
INSERT INTO "Reviews" (id, movie_id, user_id, rating, review_text, review_date)
VALUES (
$3,
$1,
$2,
5,
'Good!',
NOW()
)
""",
params: [$movieId, $userId, $reviewId]
)
}
範例 7:使用 Postgres 擴充功能
使用原生 SQL 時,您可以運用 PostGIS 等 Postgres 擴充功能,不必將複雜的幾何類型對應至 GraphQL 結構定義,也不必變更基礎資料表。
在這個範例中,假設您的餐廳應用程式有一個資料表,可將位置資料儲存在中繼資料 JSON 欄 (例如 {"latitude": 37.3688, "longitude": -122.0363})。如果已啟用 PostGIS 擴充功能,您可以使用標準 Postgres JSON 運算子 (->>) 即時擷取這些值,並將其傳遞至 PostGIS ST_MakePoint 函式。
query GetNearbyActiveRestaurants($userLong: Float!, $userLat: Float!, $maxDistanceMeters: Float!) @auth(level: USER) {
nearby: _select(
sql: """
SELECT
id,
name,
tags,
ST_Distance(
ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography,
ST_MakePoint($1, $2)::geography
) as distance_meters
FROM restaurant
WHERE active = true
AND metadata ? 'longitude' AND metadata ? 'latitude'
AND ST_DWithin(
ST_MakePoint((metadata->>'longitude')::float, (metadata->>'latitude')::float)::geography,
ST_MakePoint($1, $2)::geography,
$3
)
ORDER BY distance_meters ASC
LIMIT 10
""",
params: [$userLong, $userLat, $maxDistanceMeters]
)
}
使用用戶端 SDK 執行查詢後,結果會顯示在 data.nearby 中。
安全性最佳做法:動態 SQL 和預存程序
Data Connect 在 GraphQL 到資料庫的界線安全地將所有輸入參數化,全面保護標準 SQL 查詢免於一階 SQL 植入攻擊。不過,如果您使用 SQL 呼叫自訂 Postgres 預存程序或函式來執行動態 SQL,請務必確保內部 PL/pgSQL 程式碼會安全地處理這些參數。
如果預存程序直接將使用者輸入內容串連到 EXECUTE 字串中,就會略過參數化,並產生二階 SQL 注入弱點:
-- INSECURE: Do not concatenate parameters into dynamic strings!
CREATE OR REPLACE PROCEDURE unsafe_update(user_input TEXT)
LANGUAGE plpgsql AS $$
BEGIN
-- A malicious user_input (e.g., "val'; DROP TABLE users; --") will execute as code.
EXECUTE 'UPDATE target_table SET status = ''' || user_input || '''';
END;
$$;
如要避免這種情況,請採用下列最佳做法:
- 使用
USING子句:在預存程序中編寫動態 SQL 時,請務必使用USING子句安全地繫結資料參數。 - 使用
format()做為 ID:搭配%I旗標使用format(),確保資料庫 ID 注入安全無虞 (例如資料表名稱)。 - 嚴格限制 ID:請勿讓用戶端應用程式任意選擇資料庫 ID。如果程序需要動態 ID,請先根據 PL/pgSQL 邏輯中的硬式編碼允許清單驗證輸入內容,再執行程序。
-- SECURE: Use format() for identifiers and USING for data values
CREATE OR REPLACE PROCEDURE secure_update(target_table TEXT, new_value TEXT, row_id INT)
LANGUAGE plpgsql AS $$
BEGIN
-- Validate the dynamic table name against an allowlist
IF target_table NOT IN ('orders', 'users', 'inventory') THEN
RAISE EXCEPTION 'Invalid table name';
END IF;
-- Execute securely
EXECUTE format('UPDATE %I SET status = $1 WHERE id = $2', target_table)
USING new_value, row_id;
END;
$$;