Firebase Data Connect 提供多种与 Cloud SQL 数据库交互的方式:
- 原生 GraphQL:在
schema.gql和 Data Connect 中定义类型,将 GraphQL 操作转换为 SQL。这是标准方法,可提供强类型和架构强制结构。本页面之外的大部分 Data Connect 文档都会讨论此选项。如果可以,您应使用此方法,以便充分利用类型安全性和工具支持。 @view指令:在schema.gql中定义由自定义SELECTSQL 语句支持的 GraphQL 类型。这对于基于复杂的 SQL 逻辑创建只读的强类型视图非常有用。这些类型可像常规类型一样进行查询。请参阅@view。- 原生 SQL:直接在 . 中命名操作中嵌入 SQL 语句。
gql文件使用特殊根字段。这可提供最大的灵活性和直接控制,尤其是在以下情况下:操作不易用标准 GraphQL 表示、利用数据库特定功能或使用 PostgreSQL 扩展程序。
本指南重点介绍 Native 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 和身份验证上下文的 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(原子 get-or-create)的高级 CTE
此模式有助于确保在单个数据库事务中插入子记录(例如评价)之前,存在相关记录(例如用户或电影)。
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 调用执行动态 SQL 的自定义 Postgres 存储过程或函数,则必须确保内部 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()作为标识符:将format()与%I标志搭配使用,以安全地注入数据库标识符(例如表名称)。 - 严格允许标识符:不允许客户端应用随意选择数据库标识符。如果您的过程需要动态标识符,请在执行之前根据 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;
$$;