使用原生 SQL 實作 Firebase Data Connect 作業

Firebase Data Connect 提供多種與 Cloud SQL 資料庫互動的方式:

  • 原生 GraphQL:在 schema.gql 中定義型別,並將 GraphQL 作業轉換為 SQL。Data Connect這是標準做法,可提供嚴格型別和結構定義強制執行的結構。本頁面以外的Data Connect說明文件大多會討論這個選項。建議盡可能使用這個方法,充分發揮型別安全和工具支援的優勢。
  • @view 指令:在 schema.gql 中定義 GraphQL 型別,並以自訂 SELECT SQL 陳述式做為後端。這項功能有助於根據複雜的 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 撰寫作業,請使用 querymutation 類型的下列其中一個根欄位:

query 個欄位

欄位 說明
_select

執行 SQL 查詢,傳回零或多個資料列。

引數

  • sql:SQL 陳述式字串常值。為防止 SQL 插入攻擊,請使用位置預留位置 ($1$2 等) 做為參數值。
  • params:要繫結至預留位置的值的已排序清單。這包括常值、GraphQL 變數,以及伺服器插入的特殊內容對應 (例如 {_expr: "auth.uid"},也就是已驗證使用者的 ID)。

傳回:JSON 陣列 ([Any])。

_selectFirst

執行預期會傳回零或一個資料列的 SQL 查詢。

引數

  • sql:SQL 陳述式字串常值。為防止 SQL 插入攻擊,請使用位置預留位置 ($1$2 等) 做為參數值。
  • params:要繫結至預留位置的值的已排序清單。這包括常值、GraphQL 變數,以及伺服器插入的特殊內容對應 (例如 {_expr: "auth.uid"},也就是已驗證使用者的 ID)。

傳回:JSON 物件 (Any) 或 null

mutation 個欄位

欄位 說明
_execute

執行 DML 陳述式 (INSERT, UPDATE, DELETE)。

引數

  • sql:SQL 陳述式字串常值。如要防範 SQL 插入式攻擊,請使用位置預留位置 ($1$2 等) 做為參數值。

    您可以在這裡使用資料修改通用資料表運算式 (例如 WITH new_row AS (INSERT...)),因為這個欄位只會傳回列數。只有 _execute 支援 CTE。

  • params:要繫結至預留位置的值的已排序清單。這包括常值、GraphQL 變數,以及伺服器插入的特殊內容對應 (例如 {_expr: "auth.uid"},也就是已驗證使用者的 ID)。

傳回Int (受影響的資料列數)。

結果會忽略 RETURNING 子句。

_executeReturning

執行含有 RETURNING 子句的 DML 陳述式,並傳回零或多個資料列。

引數

  • sql:SQL 陳述式字串常值。如要防範 SQL 插入式攻擊,請使用位置預留位置 ($1$2 等) 做為參數值。不支援修改資料的通用資料表運算式。
  • params:要繫結至預留位置的值的已排序清單。這包括常值、GraphQL 變數,以及伺服器插入的特殊內容對應 (例如 {_expr: "auth.uid"},也就是已驗證使用者的 ID)。

傳回:JSON 陣列 ([Any])。

_executeReturningFirst

執行含有 RETURNING 子句的 DML 陳述式,預期會傳回零或一個資料列。

引數

  • sql:SQL 陳述式字串常值。如要防範 SQL 插入式攻擊,請使用位置預留位置 ($1$2 等) 做為參數值。不支援修改資料的通用資料表運算式。
  • params:要繫結至預留位置的值的已排序清單。這包括常值、GraphQL 變數,以及伺服器插入的特殊內容對應 (例如 {_expr: "auth.uid"},也就是已驗證使用者的 ID)。

傳回:JSON 物件 (Any) 或 null

注意:

  • 系統會使用授予 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;
$$;