تنفيذ عمليات Firebase SQL Connect باستخدام SQL الأصلية

دليل حول كتابة عمليات Firebase SQL Connect باستخدام SQL بدلاً من GraphQL page_type: guide announcement: > تتوفّر لغة SQL الأصلية كميزة في الإصدار التجريبي، ما يعني أنّها لا تخضع لاتفاقية مستوى الخدمة أو سياسة الإيقاف النهائي، وقد يتم تغييرها بطرق غير متوافقة مع الأنظمة القديمة. إذا كنت تستخدم هذه الميزة مع الإجراءات المخزّنة أو الدوال التي تنفّذ SQL ديناميكيًا، اتّبِع أفضل ممارسات الأمان الموضّحة في أسفل هذه الصفحة.

توفّر Firebase SQL Connect طرقًا متعددة للتفاعل مع قاعدة بيانات Cloud SQL:

  • GraphQL الأصلي: يمكنك تحديد الأنواع في schema.gql، وستحوّل SQL Connect عمليات GraphQL إلى SQL. هذه هي الطريقة العادية التي توفّر كتابة قوية وبنيات مفروضة على المخطط. تتناول معظم مستندات SQL Connect خارج هذه الصفحة هذا الخيار. يجب استخدام هذه الطريقة عند الإمكان للاستفادة من الأمان الكامل للأنواع ودعم الأدوات.
  • توجيه @view: يحدّد نوع GraphQL في schema.gql يستند إلى عبارة SELECT SQL مخصّصة. ويكون ذلك مفيدًا لإنشاء طرق عرض للقراءة فقط ومكتوبة بشكل صارم استنادًا إلى منطق SQL معقّد. ويمكن الاستعلام عن هذه الأنواع مثل الأنواع العادية. يمكنك الاطّلاع على @view.
  • لغة SQL الأصلية: يمكنك تضمين عبارات SQL مباشرةً في العمليات المسماة في ملفات .gql باستخدام حقول جذر خاصة. يوفّر ذلك مرونة قصوى وتحكّمًا مباشرًا، خاصةً في العمليات غير المتوافقة مع GraphQL العادية، أو الاستفادة من الميزات الخاصة بقاعدة البيانات، أو استخدام إضافات PostgreSQL. على عكس GraphQL والتوجيه @view، لا يوفّر SQL الأصلي ناتجًا مكتوبًا بشكل صارم.

يركّز هذا الدليل على الخيار 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 فقط مع التعبيرات الجدولية الشائعة.

  • 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.

ملاحظات:

  • يتم تنفيذ العمليات باستخدام الأذونات الممنوحة SQL Connect لحساب الخدمة.

قواعد البنية والقيود

تفرض لغة SQL الأصلية قواعد تحليل صارمة لضمان الأمان ومنع اختراق SQL. يُرجى مراعاة القيود التالية:

  • التعليقات: استخدِموا تعليقات الحظر (/* ... */). يُحظر استخدام التعليقات على مستوى السطر (--) لأنّها قد تؤدي إلى اقتطاع العبارات اللاحقة (مثل فلاتر الأمان) أثناء ربط طلبات البحث.
  • المَعلمات: استخدِم المَعلمات الموضعية ($1 و$2) التي تتطابق مع ترتيب مصفوفة params. المَعلمات المسماة ($id، :name) غير مدعومة.
  • السلاسل: يتم دعم سلاسل الحروف الموسّعة (E'...') والسلاسل المقتبسة بعلامة الدولار ($$...$$). لا تتوافق عمليات إلغاء الترميز في يونيكود في PostgreSQL (U&'...') مع هذا الإعداد.

المَعلمات في التعليقات

يتجاهل المحلّل كل ما هو موجود داخل تعليق على مستوى الكتلة. إذا أضفت تعليقًا إلى سطر يتضمّن مَعلمة (مثل /* WHERE id = $1 */)، عليك أيضًا إزالة هذه المَعلمة من قائمة params، وإلا ستتعذّر العملية وسيظهر الخطأ unused parameter: $1.

اصطلاحات التسمية

عند كتابة SQL الأصلية، تتفاعل مباشرةً مع قاعدة بيانات PostgreSQL، لذا عليك استخدام أسماء قاعدة البيانات الفعلية للجداول والأعمدة. بشكل تلقائي، تربط SQL Connect الأسماء في مخطط GraphQL بـ snake case في قاعدة البيانات، ما لم تخصّص بشكل صريح معرّفات Postgres باستخدام التوجيهَين @table(name) و@col(name).

إذا حدّدت نوعًا بدون توجيهات، سيتم ربط أسماء جداول وحقول GraphQL بمعرّفات snake_case Postgres التلقائية:

schema.gql queries.gql
type UserProfile {
  userId: ID!
  displayName: String
}
query GetUserProfileDefault($id: ID!) {
  profile: _selectFirst(
    sql: """
      SELECT user_id, display_name
      FROM user_profile
      WHERE user_id = $1
    """,
    params: [$id]
  )
}

تكون معرّفات PostgreSQL غير حساسة لحالة الأحرف تلقائيًا. إذا كنت تستخدم توجيهات مثل @table أو @col لتحديد اسم يحتوي على أحرف كبيرة أو أحرف مختلطة، عليك تضمين هذا المعرّف بين علامتَي اقتباس مزدوجتَين في عبارات SQL.

في المثال التالي، يجب استخدام "UserProfiles" لاسم الجدول و"profileId" للعمود userId. يتبع الحقل displayName عملية التحويل التلقائي إلى display_name:

schema.gql queries.gql
type UserProfileCustom @table(name: "UserProfiles") {
  userId: ID! @col(name: "profileId")
  displayName: String
}
query GetUserProfileCustom($id: ID!) {
  profile: _selectFirst(
    sql: """
      SELECT "profileId", display_name
      FROM "UserProfiles"
      WHERE "profileId" = $1
    """,
    params: [$id]
  )
}

أمثلة على استخدام هذه الكلمة في جملة

المثال 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 متقدّمة مع عمليات إدراج/تعديل (عملية get-or-create ذرية)

يفيد هذا النمط في ضمان توفّر السجلات التابعة (مثل المستخدمين أو الأفلام) قبل إدراج سجل فرعي (مثل مراجعة)، وكل ذلك في معاملة واحدة في قاعدة البيانات.

mutations.gql:

mutation CreateMovieCTE($movieId: UUID!, $userId: UUID!, $reviewId: UUID!) @auth(level: USER) {
  _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]
  )
}

تضمّن _executeReturning و_executeReturningFirst طلب البحث في تعبير CTE رئيسي لتنسيق الناتج بتنسيق JSON. لا يسمح PostgreSQL بتضمين تعبير CTE معدِّل للبيانات داخل عبارة أخرى معدِّلة للبيانات، ما يؤدي إلى تعذُّر تنفيذ طلب البحث.

المثال 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 الديناميكية والإجراءات المخزّنة

تُعطي SQL 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;
$$;