توفّر Firebase Data Connect طرقًا متعددة للتفاعل مع قاعدة بيانات Cloud SQL:
- GraphQL الأصلي: يتيح لك تحديد الأنواع في
schema.gqlوData Connect، ويحوّل عمليات GraphQL إلى SQL. هذه هي الطريقة العادية، التي توفّر كتابة قوية وبُنى مفروضة المخطط. تتناول معظم مستندات Data Connect خارج هذه الصفحة هذا الخيار. يجب استخدام هذه الطريقة عند الإمكان للاستفادة من ميزات منع أخطاء الكتابة ودعم الأدوات. - توجيه
@view: يحدّد نوع GraphQL فيschema.gqlيستند إلى عبارةSELECTSQL مخصّصة. ويكون ذلك مفيدًا لإنشاء طرق عرض للقراءة فقط ومكتوبة بشكل صارم استنادًا إلى منطق 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 يعرض صفر صف أو أكثر الوسيطات:
النتائج: مصفوفة JSON ( |
_selectFirst |
تنفِّذ هذه الدالة طلب بحث SQL من المتوقّع أن يعرض صفرًا أو صفًا واحدًا. الوسيطات:
القيمة المعروضة: عنصر JSON ( |
mutation حقول
| الحقل | الوصف |
|---|---|
_execute |
تنفيذ جملة DML ( الوسيطات:
المرتجعات: يتم تجاهل عبارات |
_executeReturning |
تنفّذ هذه الدالة جملة DML مع عبارة الوسيطات:
النتائج: مصفوفة JSON ( |
_executeReturningFirst |
تنفّذ هذه الدالة جملة DML مع عبارة الوسيطات:
القيمة المعروضة: عنصر JSON ( |
ملاحظات:
يتم تنفيذ العمليات باستخدام الأذونات الممنوحة لحساب خدمة 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;
$$;