تنفيذ عمليات 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.
  • Native 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، استخدِم أحد الحقول الجذرية التالية من النوعَين query أو mutation:

query حقول

الحقل الوصف
_select

تنفيذ طلب بحث SQL يعرض صفر صف أو أكثر

الوسيطات:

  • sql: السلسلة الحرفية لعبارة SQL. لمنع حقن SQL، استخدِم عناصر نائبة موضعية ($1 و$2 وما إلى ذلك) لقيم المَعلمات.
  • params: قائمة منظَّمة بالقيم التي سيتم ربطها بالعناصر النائبة. يمكن أن يشمل ذلك القيم الحرفية ومتغيّرات GraphQL وخرائط السياق الخاصة التي يتم إدخالها من الخادم، مثل {_expr: "auth.uid"} (معرّف المستخدم الذي تمّت مصادقته).

النتائج: مصفوفة JSON ([Any]).

_selectFirst

تنفِّذ هذه الدالة طلب بحث SQL من المتوقّع أن يعرض صفرًا أو صفًا واحدًا.

الوسيطات:

  • sql: السلسلة الحرفية لعبارة SQL. لمنع حقن SQL، استخدِم عناصر نائبة موضعية ($1 و$2 وما إلى ذلك) لقيم المَعلمات.
  • params: قائمة منظَّمة بالقيم التي سيتم ربطها بالعناصر النائبة. يمكن أن يشمل ذلك القيم الحرفية ومتغيّرات GraphQL وخرائط السياق الخاصة التي يتم إدخالها من الخادم، مثل {_expr: "auth.uid"} (معرّف المستخدم الذي تمّت مصادقته).

القيمة المعروضة: عنصر JSON (Any) أو null

mutation حقول

الحقل الوصف
_execute

تنفيذ جملة DML (INSERT, UPDATE, DELETE)

الوسيطات:

  • sql: السلسلة الحرفية لعبارة SQL. لمنع حقن SQL، استخدِم عناصر نائبة موضعية ($1 و$2 وما إلى ذلك) لقيم المَعلمات.

    يمكنك استخدام عبارات Common Table Expressions المعدِّلة للبيانات (مثل WITH new_row AS (INSERT...)) هنا لأنّ هذا الحقل يعرض عدد الصفوف فقط. لا تتوافق السمة _execute إلا مع CTE.

  • params: قائمة منظَّمة بالقيم التي سيتم ربطها بالعناصر النائبة. يمكن أن يشمل ذلك القيم الحرفية ومتغيّرات GraphQL وخرائط السياق الخاصة التي يتم إدخالها من الخادم، مثل {_expr: "auth.uid"} (معرّف المستخدم الذي تمّت مصادقته).

المرتجعات: Int (عدد الصفوف المتأثرة).

يتم تجاهل عبارات RETURNING في النتيجة.

_executeReturning

تنفّذ هذه الدالة جملة DML مع عبارة RETURNING، وتعرض صفر صف أو أكثر.

الوسيطات:

  • sql: السلسلة الحرفية لعبارة SQL. لمنع حقن SQL، استخدِم عناصر نائبة موضعية ($1 و$2 وما إلى ذلك) لقيم المَعلمات. لا تتوافق "تعبيرات الجداول المشتركة" التي تعدّل البيانات.
  • params: قائمة منظَّمة بالقيم التي سيتم ربطها بالعناصر النائبة. يمكن أن يشمل ذلك القيم الحرفية ومتغيّرات GraphQL وخرائط السياق الخاصة التي يتم إدخالها من الخادم، مثل {_expr: "auth.uid"} (معرّف المستخدم الذي تمّت مصادقته).

النتائج: مصفوفة JSON ([Any]).

_executeReturningFirst

تنفّذ هذه الدالة جملة DML مع عبارة RETURNING، ومن المتوقّع أن تعرض صفرًا أو صفًا واحدًا.

الوسيطات:

  • sql: السلسلة الحرفية لعبارة SQL. لمنع حقن SQL، استخدِم عناصر نائبة موضعية ($1 و$2 وما إلى ذلك) لقيم المَعلمات. لا تتوافق "تعبيرات الجداول المشتركة" التي تعدّل البيانات.
  • 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'...') والسلاسل المقتبسة بعلامة الدولار ($$...$$). لا تتوافق عمليات إلغاء الترميز في يونيكود في 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: تعبير جدول مشترك متقدّم مع عمليات إدراج/تعديل (عملية 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 الأصلية استخدام إضافات 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 injection من الدرجة الثانية:

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