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

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

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

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

يركّز هذا الدليل على خيار Native SQL.

حالات الاستخدام الشائعة لـ Native SQL

في حين يوفّر Native GraphQL ميزة منع أخطاء الكتابة الكامل، ويقدّم التوجيه @view نتائج مكتوبة بقوة لتقارير SQL للقراءة فقط، يوفّر Native SQL المرونة اللازمة لما يلي:

  • إضافات PostgreSQL: يمكنك الاستعلام عن أي إضافة مثبَّتة في PostgreSQL واستخدامها مباشرةً (مثل PostGIS للبيانات الجغرافية المكانية) بدون الحاجة إلى ربط الأنواع المعقّدة في مخطط GraphQL.
  • الاستعلامات المعقّدة: يمكنك تنفيذ SQL المعقّد باستخدام عمليات الربط والاستعلامات الفرعية، التجميع والدوال المستندة إلى النطاق والإجراءات المخزّنة.
  • معالجة البيانات (DML): يمكنك تنفيذ عمليات INSERT, UPDATE, DELETE مباشرةً. (ومع ذلك، لا تستخدم Native SQL لأوامر لغة تعريف البيانات (DDL). عليك مواصلة إجراء التعديلات على مستوى المخطط باستخدام GraphQL للحفاظ على مزامنة الخلفية وحِزم SDK التي تم إنشاؤها.)
  • الميزات الخاصة بقاعدة البيانات: يمكنك استخدام الدوال أو العوامل أو أنواع البيانات الخاصة بـ PostgreSQL.
  • تحسين الأداء: يمكنك ضبط عبارات SQL يدويًا للمسارات المهمة.

حقول جذر Native 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

ينفّذ عبارة لغة معالجة البيانات (INSERT, UPDATE, DELETE).

الوسيطات:

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

    يمكنك استخدام عبارات الجدول الشائعة التي تعدّل البيانات (على سبيل المثال، WITH new_row AS (INSERT...)) هنا لأنّ هذا الحقل يعرض عدد الصفوف فقط. لا يتيح _execute عبارات الجدول الشائعة إلا في هذا الحقل.

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

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

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

_executeReturning

ينفّذ عبارة لغة معالجة البيانات باستخدام عبارة RETURNING، ويعرض صفرًا أو أكثر من الصفوف.

الوسيطات:

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

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

_executeReturningFirst

ينفّذ عبارة لغة معالجة البيانات باستخدام عبارة RETURNING، يُتوقَّع أن يعرض صفرًا أو صفًا واحدًا.

الوسيطات:

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

النتائج: كائن JSON (Any) أو null.

ملاحظات:

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

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

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

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

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

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

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

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

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

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: عبارة جدول شائعة متقدّمة باستخدام عمليات الإدراج أو التعديل (الحصول على سجلّ أو إنشاؤه بشكلٍ ذري)

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

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 يغلّفان استعلامك في عبارة جدول شائعة رئيسية لتنسيق الناتج بتنسيق JSON. لا يسمح PostgreSQL بتضمين عبارة جدول شائعة تعدّل البيانات داخل عبارة أخرى تعدّل البيانات، ما يؤدي إلى فشل الاستعلام.

المثال 7: استخدام إضافات PostgreSQL

يتيح لك Native SQL استخدام إضافات PostgreSQL، مثل PostGIS، بدون الحاجة إلى ربط أنواع الهندسة المعقّدة في مخطط GraphQL أو تغيير الجداول الأساسية.

في هذا المثال، لنفترض أنّ تطبيق المطعم يتضمّن جدولاً يخزّن بيانات الموقع الجغرافي في عمود JSON للبيانات الوصفية (على سبيل المثال، {"latitude": 37.3688, "longitude": -122.0363}). إذا فعّلت إضافة PostGIS ، يمكنك استخدام عوامل JSON العادية في PostgreSQL (->>) لاستخراج هذه القيم أثناء التنفيذ وتمريرها إلى دالة PostGIS ST_MakePoint.

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 لاستدعاء إجراءات أو دوال مخزّنة مخصّصة في PostgreSQL تنفّذ SQL ديناميكيًا، عليك التأكّد من أنّ رمز PL/pgSQL الداخلي يعالج هذه المَعلمات بشكلٍ آمن.

إذا كان الإجراء المخزّن يربط مباشرةً إدخالات المستخدمين بسلسلة EXECUTE، فإنّه يتجاوز عملية إعداد المَعلمات ويُنشئ ثغرة أمنية لحقن SQL من الدرجة الثانية:

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