ใช้การดำเนินการ Firebase Data Connect โดยใช้ SQL ดั้งเดิม

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 แถวขึ้นไป

อาร์กิวเมนต์

  • sql: สตริงข้อความ SQL ใช้ตัวยึดตำแหน่งตามตำแหน่ง ($1, $2 และอื่นๆ) สำหรับค่าพารามิเตอร์เพื่อป้องกันการแทรก SQL
  • params: รายการค่าที่เรียงลำดับเพื่อเชื่อมโยงกับตัวยึดตำแหน่ง ซึ่งอาจรวมถึงค่าคงที่ ตัวแปร GraphQL และแผนที่บริบทพิเศษที่เซิร์ฟเวอร์แทรก เช่น {_expr: "auth.uid"} (รหัสของผู้ใช้ที่ได้รับการตรวจสอบสิทธิ์)

การคืนค่า: อาร์เรย์ JSON ([Any])

_selectFirst

เรียกใช้คำค้นหา SQL ที่คาดว่าจะแสดงผล 0 หรือ 1 แถว

อาร์กิวเมนต์

  • sql: สตริงข้อความ SQL ใช้ตัวยึดตำแหน่งตามตำแหน่ง ($1, $2 และอื่นๆ) สำหรับค่าพารามิเตอร์เพื่อป้องกันการแทรก SQL
  • params: รายการค่าที่เรียงลำดับเพื่อเชื่อมโยงกับตัวยึดตำแหน่ง ซึ่งอาจรวมถึงค่าคงที่ ตัวแปร GraphQL และแผนที่บริบทพิเศษที่เซิร์ฟเวอร์แทรก เช่น {_expr: "auth.uid"} (รหัสของผู้ใช้ที่ได้รับการตรวจสอบสิทธิ์)

การคืนค่า: ออบเจ็กต์ JSON (Any) หรือ null

mutation ฟิลด์

ช่อง คำอธิบาย
_execute

เรียกใช้คำสั่ง DML (INSERT, UPDATE, DELETE)

อาร์กิวเมนต์

  • sql: สตริงข้อความ SQL ใช้ตัวยึดตำแหน่งตามตำแหน่ง ($1, $2 และอื่นๆ) สำหรับค่าพารามิเตอร์เพื่อป้องกันการแทรก SQL

    คุณใช้ Common Table Expressions ที่แก้ไขข้อมูล (เช่น WITH new_row AS (INSERT...)) ในที่นี้ได้เนื่องจากฟิลด์นี้จะแสดงเฉพาะจำนวนแถว เฉพาะ _execute เท่านั้นที่รองรับ CTE

  • params: รายการค่าที่เรียงลำดับเพื่อเชื่อมโยงกับตัวยึดตำแหน่ง ซึ่งอาจรวมถึงค่าคงที่ ตัวแปร GraphQL และแผนที่บริบทพิเศษที่เซิร์ฟเวอร์แทรก เช่น {_expr: "auth.uid"} (รหัสของผู้ใช้ที่ได้รับการตรวจสอบสิทธิ์)

Returns: an Int (number of rows affected).

ระบบจะไม่สนใจข้อความ RETURNING ในผลลัพธ์

_executeReturning

เรียกใช้คำสั่ง DML ที่มีคําสั่ง RETURNING โดยจะส่งกลับ 0 แถวหรือมากกว่า

อาร์กิวเมนต์

  • sql: สตริงข้อความ SQL ใช้ตัวยึดตำแหน่งตามตำแหน่ง ($1, $2 และอื่นๆ) สำหรับค่าพารามิเตอร์เพื่อป้องกันการแทรก SQL ระบบไม่รองรับนิพจน์ตารางทั่วไปที่แก้ไขข้อมูล
  • params: รายการค่าที่เรียงลำดับเพื่อเชื่อมโยงกับตัวยึดตำแหน่ง ซึ่งอาจรวมถึงค่าคงที่ ตัวแปร GraphQL และแผนที่บริบทพิเศษที่เซิร์ฟเวอร์แทรก เช่น {_expr: "auth.uid"} (รหัสของผู้ใช้ที่ได้รับการตรวจสอบสิทธิ์)

การคืนค่า: อาร์เรย์ JSON ([Any])

_executeReturningFirst

เรียกใช้คำสั่ง DML ที่มีคําสั่ง RETURNING ซึ่งคาดว่าจะแสดงผล 0 หรือ 1 แถว

อาร์กิวเมนต์

  • sql: สตริงข้อความ SQL ใช้ตัวยึดตำแหน่งตามตำแหน่ง ($1, $2 และอื่นๆ) สำหรับค่าพารามิเตอร์เพื่อป้องกันการแทรก SQL ระบบไม่รองรับนิพจน์ตารางทั่วไปที่แก้ไขข้อมูล
  • params: รายการค่าที่เรียงลำดับเพื่อเชื่อมโยงกับตัวยึดตำแหน่ง ซึ่งอาจรวมถึงค่าคงที่ ตัวแปร GraphQL และแผนที่บริบทพิเศษที่เซิร์ฟเวอร์แทรก เช่น {_expr: "auth.uid"} (รหัสของผู้ใช้ที่ได้รับการตรวจสอบสิทธิ์)

การคืนค่า: ออบเจ็กต์ JSON (Any) หรือ null

หมายเหตุ:

  • ระบบจะดำเนินการโดยใช้สิทธิ์ที่มอบให้กับบัญชีบริการ 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;
$$;