Menerapkan operasi Firebase Data Connect menggunakan SQL native

Firebase Data Connect menawarkan beberapa cara untuk berinteraksi dengan database Cloud SQL:

  • GraphQL Native: Menentukan jenis di schema.gql dan Data Connect menerjemahkan operasi GraphQL Anda ke dalam SQL. Ini adalah pendekatan standar, yang menawarkan pengetikan yang kuat dan struktur yang diterapkan skemanya. Sebagian besar dokumentasi Data Connect di luar halaman ini membahas opsi ini. Jika memungkinkan, Anda harus menggunakan metode ini untuk memanfaatkan keamanan jenis dan dukungan alat secara penuh.
  • Direktif @view: Tentukan jenis GraphQL di schema.gql yang didukung oleh pernyataan SQL SELECT kustom. Hal ini berguna untuk membuat tampilan hanya baca, berjenis kuat berdasarkan logika SQL yang kompleks. Jenis ini dapat dikueri seperti jenis reguler. Lihat @view.
  • SQL Native: Sematkan pernyataan SQL langsung dalam operasi bernama di .gql menggunakan kolom root khusus. Hal ini memberikan fleksibilitas dan kontrol langsung maksimum, terutama untuk operasi yang tidak mudah dinyatakan dalam GraphQL standar, memanfaatkan fitur khusus database, atau menggunakan ekstensi PostgreSQL.

Panduan ini berfokus pada opsi SQL Native.

Kasus penggunaan umum untuk SQL native

Meskipun GraphQL native memberikan keamanan jenis penuh, dan direktif @view menawarkan hasil yang sangat diketik untuk laporan SQL hanya baca, SQL native memberikan fleksibilitas yang diperlukan untuk:

  • Ekstensi PostgreSQL: Lakukan kueri dan gunakan ekstensi PostgreSQL yang diinstal secara langsung (seperti PostGIS untuk data geospasial) tanpa perlu memetakan jenis yang kompleks dalam skema GraphQL Anda.
  • Kueri Kompleks: Jalankan SQL rumit dengan gabungan, subkueri, agregasi, fungsi jendela, dan prosedur tersimpan.
  • Manipulasi Data (DML): Lakukan operasi INSERT, UPDATE, DELETE secara langsung. (Namun, jangan gunakan SQL native untuk perintah Bahasa Definisi Data (DDL). Anda harus terus melakukan perubahan tingkat skema menggunakan GraphQL untuk menjaga sinkronisasi backend dan SDK yang dihasilkan.)
  • Fitur Khusus Database: Manfaatkan fungsi, operator, atau jenis data yang unik untuk PostgreSQL.
  • Pengoptimalan Performa: Sesuaikan pernyataan SQL secara manual untuk jalur penting.

Kolom root SQL Native

Untuk menulis operasi dengan SQL, gunakan salah satu kolom root berikut dari jenis query atau mutation:

query kolom

Kolom Deskripsi
_select

Menjalankan kueri SQL yang menampilkan nol baris atau lebih.

Argumen:

  • sql: Literal string pernyataan SQL. Untuk mencegah injeksi SQL, gunakan placeholder posisi ($1, $2, dan sebagainya) untuk nilai parameter.
  • params: Daftar nilai yang diurutkan untuk diikat ke placeholder. Hal ini dapat mencakup literal, variabel GraphQL, dan peta konteks khusus yang disisipkan server seperti {_expr: "auth.uid"} (ID pengguna yang diautentikasi).

Respons: array JSON ([Any]).

_selectFirst

Menjalankan kueri SQL yang diharapkan menampilkan nol atau satu baris.

Argumen:

  • sql: Literal string pernyataan SQL. Untuk mencegah injeksi SQL, gunakan placeholder posisi ($1, $2, dan sebagainya) untuk nilai parameter.
  • params: Daftar nilai yang diurutkan untuk diikat ke placeholder. Hal ini dapat mencakup literal, variabel GraphQL, dan peta konteks khusus yang disisipkan server seperti {_expr: "auth.uid"} (ID pengguna yang diautentikasi).

Menampilkan: objek JSON (Any) atau null.

mutation kolom

Kolom Deskripsi
_execute

Mengeksekusi pernyataan DML (INSERT, UPDATE, DELETE).

Argumen:

  • sql: Literal string pernyataan SQL. Untuk mencegah injeksi SQL, gunakan placeholder posisi ($1, $2, dan sebagainya) untuk nilai parameter.

    Anda dapat menggunakan Ekspresi Tabel Umum (Common Table Expressions) pengubah data (misalnya, WITH new_row AS (INSERT...)) di sini karena kolom ini hanya menampilkan jumlah baris. Hanya _execute yang mendukung CTE.

  • params: Daftar nilai yang diurutkan untuk diikat ke placeholder. Hal ini dapat mencakup literal, variabel GraphQL, dan peta konteks khusus yang disisipkan server seperti {_expr: "auth.uid"} (ID pengguna yang diautentikasi).

Returns: Int (jumlah baris yang terpengaruh).

Klausul RETURNING diabaikan dalam hasilnya.

_executeReturning

Mengeksekusi pernyataan DML dengan klausa RETURNING, yang menampilkan nol baris atau lebih.

Argumen:

  • sql: Literal string pernyataan SQL. Untuk mencegah injeksi SQL, gunakan placeholder posisi ($1, $2, dan sebagainya) untuk nilai parameter. Ekspresi Tabel Umum yang mengubah data tidak didukung.
  • params: Daftar nilai yang diurutkan untuk diikat ke placeholder. Hal ini dapat mencakup literal, variabel GraphQL, dan peta konteks khusus yang disisipkan server seperti {_expr: "auth.uid"} (ID pengguna yang diautentikasi).

Respons: array JSON ([Any]).

_executeReturningFirst

Mengeksekusi pernyataan DML dengan klausa RETURNING, yang diharapkan menampilkan nol atau satu baris.

Argumen:

  • sql: Literal string pernyataan SQL. Untuk mencegah injeksi SQL, gunakan placeholder posisi ($1, $2, dan sebagainya) untuk nilai parameter. Ekspresi Tabel Umum yang mengubah data tidak didukung.
  • params: Daftar nilai yang diurutkan untuk diikat ke placeholder. Hal ini dapat mencakup literal, variabel GraphQL, dan peta konteks khusus yang disisipkan server seperti {_expr: "auth.uid"} (ID pengguna yang diautentikasi).

Menampilkan: objek JSON (Any) atau null.

Catatan:

  • Operasi dijalankan menggunakan izin yang diberikan ke akun layanan Data Connect.

  • Jika Anda secara eksplisit menetapkan nama tabel menggunakan direktif @table (@table(name: "ExampleTable")), Anda juga harus menyertakan nama tabel dalam tanda petik dalam pernyataan SQL (SELECT field FROM "ExampleTable" ...).

    Tanpa tanda petik, Data Connect akan mengonversi nama tabel menjadi snake case (example_table).

Aturan & batasan sintaksis

SQL Native menerapkan aturan parsing yang ketat untuk memastikan keamanan dan mencegah injeksi SQL. Perhatikan batasan berikut:

  • Komentar: Gunakan komentar blok (/* ... */). Komentar baris (--) dilarang karena dapat memotong klausa berikutnya (seperti filter keamanan) selama penggabungan kueri.
  • Parameter: Gunakan parameter posisi ($1, $2) yang cocok dengan urutan array params. Parameter bernama ($id, :name) tidak didukung.
  • String: Literal string yang diperluas (E'...') dan string yang diberi tanda kutip dolar ($$...$$) didukung. Escape Unicode PostgreSQL (U&'...') tidak didukung.

Parameter dalam komentar

Parser mengabaikan semua yang ada di dalam komentar blok. Jika Anda mengomentari baris yang berisi parameter (misalnya, /* WHERE id = $1 */), Anda juga harus menghapus parameter tersebut dari daftar params, atau operasi akan gagal dengan error unused parameter: $1.

Contoh

Contoh 1: SELECT dasar dengan pemberian alias kolom

Anda dapat membuat alias kolom root (misalnya, movies: _select) untuk membuat respons klien lebih bersih (data.movies, bukan 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]
  )
}

Setelah menjalankan kueri menggunakan SDK klien, hasilnya akan ada di data.movies.

Contoh 2: UPDATE dasar

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]
  )
}

Setelah menjalankan mutasi menggunakan SDK klien, jumlah baris yang terpengaruh akan berada di data._execute.

Contoh 3: Agregasi dasar

queries.gql:

query GetTotalReviewCount @auth(level: PUBLIC) {
  stats: _selectFirst(
    sql: "SELECT COUNT(*) as total_reviews FROM \"Reviews\""
  )
}

Setelah menjalankan kueri menggunakan SDK klien, hasilnya akan ada di data.stats.total_reviews.

Contoh 4: Agregasi lanjutan dengan 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: []
  )
}

Setelah menjalankan kueri menggunakan SDK klien, hasilnya akan ada di data._select.

Contoh 5: UPDATE dengan RETURNING dan Konteks Auth

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" }]
  )
}

Setelah menjalankan mutasi menggunakan SDK klien, data postingan yang diperbarui akan ada di data.updatedReview.

Contoh 6: CTE lanjutan dengan operasi upsert (get-or-create atomik)

Pola ini berguna untuk memastikan keberadaan rekaman yang bergantung (seperti Pengguna atau Film) sebelum menyisipkan rekaman turunan (seperti Ulasan), semuanya dalam satu transaksi database.

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]
  )
}

Contoh 7: Menggunakan ekstensi Postgres

SQL Native memungkinkan Anda menggunakan ekstensi Postgres, seperti PostGIS, tanpa perlu memetakan jenis geometri yang kompleks ke dalam skema GraphQL atau mengubah tabel pokok.

Dalam contoh ini, misalkan aplikasi restoran Anda memiliki tabel yang menyimpan data lokasi dalam kolom JSON metadata (misalnya, {"latitude": 37.3688, "longitude": -122.0363}). Jika Anda telah mengaktifkan ekstensi PostGIS, Anda dapat menggunakan operator JSON Postgres standar (->>) untuk mengekstrak nilai ini secara langsung dan meneruskannya ke fungsi 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]
  )
}

Setelah menjalankan kueri menggunakan SDK klien, hasilnya akan ada di data.nearby.

Praktik terbaik keamanan: SQL dinamis & prosedur tersimpan

Data Connect memparameterkan semua input secara aman di batas GraphQL ke database, sehingga sepenuhnya melindungi kueri SQL standar Anda dari injeksi SQL tingkat pertama. Namun, jika Anda menggunakan SQL untuk memanggil prosedur atau fungsi tersimpan Postgres kustom yang menjalankan SQL dinamis, Anda harus memastikan kode PL/pgSQL internal Anda menangani parameter ini dengan aman.

Jika prosedur tersimpan Anda secara langsung menggabungkan input pengguna ke dalam string EXECUTE, prosedur tersebut akan melewati parameterisasi dan membuat kerentanan injeksi SQL tingkat kedua:

-- 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;
$$;

Untuk menghindarinya, ikuti praktik terbaik berikut:

  • Gunakan klausa USING: Saat menulis SQL dinamis dalam prosedur tersimpan, selalu gunakan klausa USING untuk mengikat parameter data dengan aman.
  • Gunakan format() untuk ID: Gunakan format() dengan tanda %I untuk injeksi ID database yang aman (seperti nama tabel).
  • Izinkan ID secara ketat: Jangan biarkan aplikasi klien memilih ID database secara arbitrer. Jika prosedur Anda memerlukan ID dinamis, validasi input terhadap daftar yang diizinkan yang di-hard code dalam logika PL/pgSQL Anda sebelum dieksekusi.
-- 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;
$$;