Firebase Data Connect มีวิธีมากมายในการโต้ตอบกับฐานข้อมูล Cloud SQL ดังนี้
- GraphQL เนทีฟ: กำหนดประเภทใน
schema.gqlและ Data Connect แปลงการดำเนินการ GraphQL เป็น SQL ซึ่งเป็นแนวทางมาตรฐาน ที่ให้การพิมพ์ที่รัดกุมและโครงสร้างที่บังคับใช้สคีมา เอกสารประกอบส่วนใหญ่ Data Connectนอกหน้านี้จะอธิบายตัวเลือกนี้ หากเป็นไปได้ คุณควรใช้วิธีนี้เพื่อใช้ประโยชน์จากความปลอดภัยของประเภทและการรองรับเครื่องมืออย่างเต็มที่ @viewคำสั่ง: กำหนดประเภท GraphQL ในschema.gqlที่มีSELECTคำสั่ง SQL ที่กำหนดเองเป็นข้อมูลสำรอง ซึ่งมีประโยชน์ในการสร้างมุมมองแบบอ่านอย่างเดียว ที่มีการพิมพ์อย่างเข้มงวดโดยอิงตามตรรกะ SQL ที่ซับซ้อน ประเภทเหล่านี้สามารถค้นหาได้ เหมือนกับประเภทปกติ ดู@view- SQL ดั้งเดิม: ฝังคำสั่ง SQL โดยตรงใน
การดำเนินการที่มีชื่อใน
gqlโดยใช้ฟิลด์รูทพิเศษ ซึ่งจะช่วยให้มีความยืดหยุ่นสูงสุดและควบคุมได้โดยตรง โดยเฉพาะอย่างยิ่งสำหรับการดำเนินการที่แสดงใน GraphQL มาตรฐานได้ยาก การใช้ประโยชน์จากฟีเจอร์เฉพาะของฐานข้อมูล หรือการใช้ส่วนขยาย PostgreSQL
คู่มือนี้มุ่งเน้นที่ตัวเลือก SQL ดั้งเดิม
กรณีการใช้งานทั่วไปสำหรับ SQL ดั้งเดิม
แม้ว่า GraphQL เนทีฟจะให้ความปลอดภัยของประเภทอย่างเต็มที่ และ Directive @view จะให้ผลลัพธ์ที่มีการพิมพ์อย่างเข้มงวดสำหรับรายงาน SQL แบบอ่านอย่างเดียว แต่ SQL เนทีฟจะให้ความยืดหยุ่นที่จำเป็นสำหรับสิ่งต่อไปนี้
- ส่วนขยาย PostgreSQL: ค้นหาและใช้ส่วนขยาย PostgreSQL ที่ติดตั้ง (เช่น
PostGISสำหรับข้อมูลเชิงพื้นที่) ได้โดยตรงโดยไม่ต้องแมปประเภทที่ซับซ้อนในสคีมา GraphQL - การค้นหาที่ซับซ้อน: เรียกใช้ SQL ที่ซับซ้อนด้วย JOIN, การค้นหาย่อย การรวม ฟังก์ชันหน้าต่าง และกระบวนการที่จัดเก็บไว้
- การจัดการข้อมูล (DML): ดำเนินการ
INSERT, UPDATE, DELETEโดยตรง (อย่างไรก็ตาม อย่าใช้ SQL ดั้งเดิมสำหรับคำสั่ง Data Definition Language (DDL) คุณต้องทำการเปลี่ยนแปลงระดับสคีมาต่อไปโดยใช้ GraphQL เพื่อ ให้แบ็กเอนด์และ SDK ที่สร้างขึ้นซิงค์กัน) - ฟีเจอร์เฉพาะฐานข้อมูล: ใช้ฟังก์ชัน ตัวดำเนินการ หรือประเภทข้อมูล เฉพาะสำหรับ PostgreSQL
- การเพิ่มประสิทธิภาพ: ปรับแต่งคำสั่ง SQL ด้วยตนเองสำหรับเส้นทางที่สำคัญ
ฟิลด์รูท SQL ดั้งเดิม
หากต้องการเขียนการดำเนินการด้วย SQL ให้ใช้ฟิลด์รูทรายการใดรายการหนึ่งต่อไปนี้ของประเภท query หรือ mutation
query ฟิลด์
| ช่อง | คำอธิบาย |
|---|---|
_select |
เรียกใช้การค้นหา SQL ที่ส่งกลับแถว 0 แถวขึ้นไป อาร์กิวเมนต์
การคืนค่า: อาร์เรย์ JSON ( |
_selectFirst |
เรียกใช้คำค้นหา SQL ที่คาดว่าจะแสดงผล 0 หรือ 1 แถว อาร์กิวเมนต์
การคืนค่า: ออบเจ็กต์ JSON ( |
mutation ฟิลด์
| ช่อง | คำอธิบาย |
|---|---|
_execute |
เรียกใช้คำสั่ง DML ( อาร์กิวเมนต์
Returns: an ระบบจะไม่สนใจข้อความ |
_executeReturning |
เรียกใช้คำสั่ง DML ที่มีคําสั่ง อาร์กิวเมนต์
การคืนค่า: อาร์เรย์ JSON ( |
_executeReturningFirst |
เรียกใช้คำสั่ง DML ที่มีคําสั่ง อาร์กิวเมนต์
การคืนค่า: ออบเจ็กต์ JSON ( |
หมายเหตุ:
ระบบจะดำเนินการโดยใช้สิทธิ์ที่มอบให้กับบัญชีบริการ Data Connect
หากตั้งชื่อตารางอย่างชัดแจ้งโดยใช้
@tableคำสั่ง (@table(name: "ExampleTable")) คุณต้องใส่ชื่อตารางในเครื่องหมายคำพูดในคำสั่ง SQL ด้วย (SELECT field FROM "ExampleTable" ...)หากไม่มีเครื่องหมายคำพูด Data Connect จะแปลงชื่อตาราง เป็นรูปแบบ Snake Case (
example_table)
กฎไวยากรณ์และข้อจำกัด
SQL ดั้งเดิมจะบังคับใช้กฎการแยกวิเคราะห์ที่เข้มงวดเพื่อให้มั่นใจในความปลอดภัยและป้องกันการแทรก SQL โปรดรับทราบข้อจำกัดต่อไปนี้
- ความคิดเห็น: ใช้ความคิดเห็นแบบบล็อก (
/* ... */) ห้ามใช้ความคิดเห็นแบบบรรทัด (--) เนื่องจากอาจตัดข้อความที่ตามมา (เช่น ตัวกรองความปลอดภัย) ในระหว่างการต่อคำค้นหา - พารามิเตอร์: ใช้พารามิเตอร์ตำแหน่ง (
$1,$2) ที่ตรงกับลำดับอาร์เรย์paramsระบบไม่รองรับพารามิเตอร์ที่มีชื่อ ($id,:name) - สตริง: ระบบรองรับสตริงลิเทอรัลแบบขยาย (
E'...') และสตริงที่อ้างอิงด้วยเครื่องหมายดอลลาร์ ($$...$$) ไม่รองรับการหลีกอักขระ Unicode ของ PostgreSQL (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: UPDATE ด้วย RETURNING และบริบทการตรวจสอบสิทธิ์
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: 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 ดั้งเดิมช่วยให้คุณใช้ส่วนขยาย Postgres เช่น PostGIS ได้โดยไม่ต้อง แมปประเภทเรขาคณิตที่ซับซ้อนลงในสคีมา GraphQL หรือแก้ไขตาราง พื้นฐาน
ในตัวอย่างนี้ สมมติว่าแอปภัตตาคารมีตารางที่จัดเก็บข้อมูลสถานที่ตั้ง
ในคอลัมน์ JSON ของข้อมูลเมตา (เช่น {"latitude": 37.3688, "longitude": -122.0363})
หากเปิดใช้ส่วนขยาย PostGIS
คุณจะใช้ตัวดำเนินการ JSON มาตรฐานของ Postgres (->>) เพื่อดึงค่าเหล่านี้ได้
ทันทีและส่งไปยังฟังก์ชัน ST_MakePoint ของ PostGIS
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 ระดับที่ 2
-- 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;
$$;