Cloud Firestore — نمذجة البيانات وعمليات CRUD
Cloud Firestore — نمذجة البيانات وعمليات CRUD
Cloud Firestore هي قاعدة بيانات NoSQL مرنة وقابلة للتوسع مبنية لتطبيقات الجوال والويب. على عكس قواعد البيانات العلائقية، تنظّم Firestore البيانات في مجموعات (collections) ووثائق (documents). فهم هذا الهيكل — وقيوده — هو مفتاح تصميم مخطط يعمل بكفاءة ويتوسع بسلاسة.
المجموعات والوثائق والمجموعات الفرعية
تخزّن Firestore البيانات في تسلسل هرمي: تحتوي المجموعة على وثيقة أو أكثر، ويمكن لكل وثيقة أن تحتوي على مجموعات فرعية متداخلة. الوثيقة هي كائن يشبه JSON بحقول مسمّاة. لكل وثيقة معرّف نصي مُولَّد تلقائياً (أو يحدده المستخدم) وتوجد في مسار مثل users/uid123.
- المجموعة — حاوية للوثائق (مثل
usersوposts) - الوثيقة — سجل واحد بحقول مكتوبة (String، Number، Boolean، Timestamp، Array، Map، Reference)
- المجموعة الفرعية — مجموعة متداخلة داخل وثيقة (مثل
users/uid123/orders)
نموذج بيانات عملي — تطبيق ملاحظات
في هذا الدرس سنصمّم نموذج بيانات لتطبيق ملاحظات بسيط. كل مستخدم مصادَق عليه يمتلك مجموعة من الملاحظات. يبدو المخطط هكذا:
مخطط Firestore (مفاهيمي)
// مجموعة المستوى الأعلى: notes
// معرّف الوثيقة: مُولَّد تلقائياً
// المسار: notes/{noteId}
{
"title": "جدول أعمال الاجتماع",
"body": "مناقشة أهداف الربع الثالث...",
"authorId": "uid_abc123",
"tags": ["عمل", "ربع_3"],
"createdAt": Timestamp,
"updatedAt": Timestamp
}
إضافة حزمة cloud_firestore
أضف التبعية في pubspec.yaml ثم نفّذ flutter pub get:
pubspec.yaml
dependencies:
flutter:
sdk: flutter
firebase_core: ^3.0.0
cloud_firestore: ^5.0.0
تأكد من استدعاء Firebase.initializeApp() في main() قبل الوصول إلى Firestore (تمت تغطيته في الدرس الأول).
عمليات CRUD مع cloud_firestore
تستخدم العمليات الأربع الأساسية — الإنشاء والقراءة والتحديث والحذف — المفرد FirebaseFirestore.instance. الواجهة البرمجية غير متزامنة؛ كل عملية كتابة تُعيد Future وكل قراءة تُعيد إما Future (جلب مرة واحدة) أو Stream (مستمع في الوقت الفعلي).
فئة خدمة CRUD الكاملة
import 'package:cloud_firestore/cloud_firestore.dart';
class NoteService {
final FirebaseFirestore _db = FirebaseFirestore.instance;
final String _col = 'notes';
// الإنشاء — إضافة وثيقة جديدة بمعرّف مُولَّد تلقائياً
Future<DocumentReference> createNote({
required String title,
required String body,
required String authorId,
List<String> tags = const [],
}) async {
final data = {
'title': title,
'body': body,
'authorId': authorId,
'tags': tags,
'createdAt': FieldValue.serverTimestamp(),
'updatedAt': FieldValue.serverTimestamp(),
};
return _db.collection(_col).add(data);
}
// القراءة — جلب وثيقة واحدة بالمعرّف
Future<Map<String, dynamic>?> getNoteById(String noteId) async {
final snap = await _db.collection(_col).doc(noteId).get();
if (!snap.exists) return null;
return {'id': snap.id, ...?snap.data()};
}
// القراءة — بث في الوقت الفعلي لجميع ملاحظات المستخدم
Stream<QuerySnapshot> watchUserNotes(String authorId) {
return _db
.collection(_col)
.where('authorId', isEqualTo: authorId)
.orderBy('updatedAt', descending: true)
.snapshots();
}
// التحديث — دمج حقول محددة (لا يُستبدل المستند بالكامل)
Future<void> updateNote(
String noteId, {
String? title,
String? body,
List<String>? tags,
}) async {
final patch = <String, dynamic>{
'updatedAt': FieldValue.serverTimestamp(),
if (title != null) 'title': title,
if (body != null) 'body': body,
if (tags != null) 'tags': tags,
};
await _db.collection(_col).doc(noteId).update(patch);
}
// الحذف — إزالة الوثيقة نهائياً
Future<void> deleteNote(String noteId) async {
await _db.collection(_col).doc(noteId).delete();
}
}
update() على set() عندما تريد تغيير بعض الحقول فقط. set() بدون SetOptions(merge: true) سيُستبدل المستند بالكامل، محذوفاً بصمت الحقول التي لم تُضمّنها.معالجة الأخطاء
قد تفشل عمليات Firestore بسبب مشاكل الشبكة أو رفض الأذونات (قواعد الأمان) أو الاستعلامات غير الصالحة. احرص دائماً على التفاف الاستدعاءات في try/catch ومعالجة FirebaseException:
نمط معالجة الأخطاء
Future<void> safeDelete(String noteId) async {
try {
await _db.collection('notes').doc(noteId).delete();
} on FirebaseException catch (e) {
// e.code نص مثل 'permission-denied' أو 'not-found'
debugPrint('خطأ في Firestore [${e.code}]: ${e.message}');
rethrow; // دع طبقة واجهة المستخدم تقرر كيفية عرض الخطأ
}
}
FirebaseException.code مثل permission-denied أو unavailable إلى رسائل ودّية ومترجَمة في طبقة واجهة المستخدم.الخلاصة
في هذا الدرس تعلّمت كيف يعمل التسلسل الهرمي للمجموعات والوثائق في Firestore، وكيف تصمّم مخططاً عملياً، وكيف تُنفّذ عمليات CRUD الأربع باستخدام حزمة cloud_firestore. أبرز النقاط:
- استخدم المجموعات لقوائم الكيانات المتشابهة؛ واستخدم الوثائق للسجلات الفردية.
add()يُولّد معرّفاً تلقائياً؛doc(id).set()يتيح لك تحديد المعرّف بنفسك.update()يدمج التغييرات؛set()بدون دمج يُستبدل المستند بالكامل.FieldValue.serverTimestamp()هو الطريقة الصحيحة لتسجيل أوقات الإنشاء والتحديث.- احرص دائماً على معالجة
FirebaseExceptionوتحويل رموز الأخطاء إلى رسائل مناسبة للمستخدم.