(اختياري) إنشاء نموذج أوّلي واختباره باستخدام "مجموعة أدوات المحاكاة المحلية" من Firebase
قبل التحدّث عن كيفية قراءة تطبيقك من قاعدة بيانات الوقت الفعلي والكتابة فيها، دعنا نقدّم مجموعة من الأدوات التي يمكنك استخدامها لإنشاء نماذج أولية واختبار وظائف قاعدة بيانات الوقت الفعلي، وهي مجموعة أدوات المحاكاة من Firebase. إذا كنت بصدد تجربة نماذج بيانات مختلفة أو تحسين قواعد الأمان أو محاولة العثور على الطريقة الأكثر فعالية من حيث التكلفة للتفاعل مع الخلفية، قد يكون من المفيد أن تتمكّن من العمل محليًا بدون نشر الخدمات المباشرة.
يُعدّ محاكي قاعدة بيانات الوقت الفعلي جزءًا من مجموعة أدوات المحاكاة، ما يتيح لتطبيقك التفاعل مع المحتوى والإعدادات المحاكية لقاعدة البيانات، بالإضافة إلى موارد المشروع المحاكية (الوظائف وقواعد البيانات الأخرى وقواعد الأمان) بشكل اختياري.emulator_suite_short
يتطلّب استخدام محاكي قاعدة بيانات الوقت الفعلي بضع خطوات فقط:
- إضافة سطر من الرمز البرمجي إلى إعدادات الاختبار في تطبيقك للاتصال بالمحاكي
- من جذر دليل مشروع على جهاز المستخدم، نفِّذ الأمر
firebase emulators:start. - إجراء مكالمات من الرمز الأولي لتطبيقك باستخدام حزمة تطوير برامج (SDK) لمنصة قاعدة بيانات الوقت الفعلي كالمعتاد، أو باستخدام REST API الخاصة بقاعدة بيانات الوقت الفعلي.
يتوفّر شرح تفصيلي يتضمّن قاعدة بيانات الوقت الفعلي وCloud Functions. ننصحك أيضًا بالاطّلاع على مقدمة عن مجموعة أدوات المحاكاة.
الحصول على DatabaseReference
لقراءة البيانات أو كتابتها في قاعدة البيانات، تحتاج إلى مثيل من DatabaseReference:
DatabaseReference ref = FirebaseDatabase.instance.ref();
كتابة البيانات
يتناول هذا المستند أساسيات قراءة بيانات Firebase وكتابتها.
تتم كتابة بيانات Firebase إلى DatabaseReference واسترجاعها من خلال انتظار الأحداث التي تشير إليها المَرجعية أو الاستماع إليها. يتم إصدار الأحداث
مرة واحدة للحالة الأولية للبيانات ومرة أخرى كلما تغيرت البيانات.
عمليات الكتابة الأساسية
بالنسبة إلى عمليات الكتابة الأساسية، يمكنك استخدام set() لحفظ البيانات في مرجع محدّد، مع استبدال أي بيانات حالية في هذا المسار. يمكنك ضبط مرجع
لأنواع البيانات التالية: String وboolean وint وdouble وMap وList.
على سبيل المثال، يمكنك إضافة مستخدم لديه set() على النحو التالي:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.set({
"name": "John",
"age": 18,
"address": {
"line1": "100 Mountain View"
}
});
يؤدي استخدام set() بهذه الطريقة إلى الكتابة فوق البيانات في الموقع الجغرافي المحدّد،
بما في ذلك أي عقد فرعية. ومع ذلك، يظل بإمكانك تعديل عنصر فرعي بدون إعادة كتابة العنصر بأكمله. إذا أردت السماح للمستخدمين بتعديل ملفاتهم الشخصية، يمكنك تعديل اسم المستخدم على النحو التالي:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Only update the age, leave the name and address!
await ref.update({
"age": 19,
});
تقبل الطريقة update() مسارًا فرعيًا للعُقد، ما يتيح لك تعديل عُقد متعددة في قاعدة البيانات في الوقت نفسه:
DatabaseReference ref = FirebaseDatabase.instance.ref("users");
await ref.update({
"123/age": 19,
"123/address/line1": "1 Mountain View",
});
قراءة البيانات
قراءة البيانات من خلال رصد أحداث القيمة
لقراءة البيانات في مسار معيّن والاستماع إلى التغييرات، استخدِم السمة onValue الخاصة بـ DatabaseReference للاستماع إلى DatabaseEvent.
يمكنك استخدام DatabaseEvent لقراءة البيانات في مسار معيّن،
كما هي في وقت وقوع الحدث. يتم تشغيل هذا الحدث مرة واحدة عند ربط أداة الاستماع، ومرة أخرى في كل مرة تتغير فيها البيانات، بما في ذلك أي عناصر فرعية. يحتوي الحدث على السمة snapshot التي تتضمّن جميع البيانات في هذا الموقع الجغرافي، بما في ذلك بيانات العناصر التابعة. في حال عدم توفّر بيانات، ستكون قيمة السمة exists في اللقطة false، وستكون قيمة السمة value فارغة.
يوضّح المثال التالي تطبيقًا للتدوين على وسائل التواصل الاجتماعي يسترد تفاصيل منشور من قاعدة البيانات:
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
يتلقّى المستمع DataSnapshot الذي يحتوي على البيانات في الموقع المحدّد في قاعدة البيانات في وقت الحدث في السمة value.
قراءة البيانات مرة واحدة
القراءة مرة واحدة باستخدام get()
تم تصميم حزمة تطوير البرامج (SDK) لإدارة التفاعلات مع خوادم قواعد البيانات سواء كان تطبيقك متصلاً بالإنترنت أو غير متصل.
بشكل عام، يجب استخدام تقنيات أحداث القيمة الموضّحة أعلاه لقراءة البيانات من أجل تلقّي إشعارات بشأن التعديلات على البيانات من الخلفية. تؤدي هذه الأساليب إلى خفض معدّل الاستخدام والفوترة، كما أنّها محسّنة لمنح المستخدمين أفضل تجربة عند الاتصال بالإنترنت أو عدم الاتصال به.
إذا كنت بحاجة إلى البيانات مرة واحدة فقط، يمكنك استخدام get() للحصول على نبذة عن البيانات من قاعدة البيانات. إذا تعذّر على get() عرض قيمة الخادم لأي سبب، سيتحقّق العميل من ذاكرة التخزين المؤقت في وحدة التخزين المحلية وسيعرض خطأً إذا لم يتم العثور على القيمة.
يوضّح المثال التالي كيفية استرداد اسم المستخدم المتاح للجميع مرة واحدة من قاعدة البيانات:
final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
print(snapshot.value);
} else {
print('No data available.');
}
يمكن أن يؤدي الاستخدام غير الضروري لـ get() إلى زيادة استخدام معدل نقل البيانات وانخفاض الأداء، ويمكن تجنُّب ذلك باستخدام متتبِّع في الوقت الفعلي كما هو موضّح أعلاه.
قراءة البيانات مرة واحدة باستخدام once()
في بعض الحالات، قد تحتاج إلى عرض القيمة من ذاكرة التخزين المؤقت المحلية على الفور، بدلاً من التحقّق من وجود قيمة معدَّلة على الخادم. في هذه الحالات، يمكنك استخدام once() للحصول على البيانات من ذاكرة التخزين المؤقت للقرص المحلي على الفور.
ويكون ذلك مفيدًا للبيانات التي يجب تحميلها مرة واحدة فقط ولا يُتوقّع أن تتغير بشكل متكرر أو تتطلب استماعًا نشطًا. على سبيل المثال، يستخدم تطبيق التدوين في الأمثلة السابقة هذه الطريقة لتحميل الملف الشخصي للمستخدم عندما يبدأ في تأليف مشاركة جديدة:
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
تعديل البيانات أو حذفها
تعديل حقول معيّنة
لكتابة بيانات إلى عناصر فرعية معيّنة من عقدة في الوقت نفسه بدون الكتابة فوق العُقد الفرعية الأخرى، استخدِم الطريقة update().
عند استدعاء update()، يمكنك تعديل قيم العناصر الفرعية ذات المستوى الأدنى من خلال تحديد مسار للمفتاح. إذا كانت البيانات مخزَّنة في مواقع جغرافية متعددة لتوسيع نطاقها بشكل أفضل، يمكنك تعديل جميع مثيلات هذه البيانات باستخدام توزيع البيانات. على سبيل المثال، قد يرغب تطبيق تدوين على وسائل التواصل الاجتماعي في إنشاء مشاركة وتعديلها في الوقت نفسه لتظهر في خلاصة الأنشطة الأخيرة وخلاصة أنشطة المستخدم الذي نشر المشاركة. لإجراء ذلك، يستخدم تطبيق التدوين رمزًا برمجيًا على النحو التالي:
void writeNewPost(String uid, String username, String picture, String title,
String body) async {
// A post entry.
final postData = {
'author': username,
'uid': uid,
'body': body,
'title': title,
'starCount': 0,
'authorPic': picture,
};
// Get a key for a new Post.
final newPostKey =
FirebaseDatabase.instance.ref().child('posts').push().key;
// Write the new post's data simultaneously in the posts list and the
// user's post list.
final Map<String, Map> updates = {};
updates['/posts/$newPostKey'] = postData;
updates['/user-posts/$uid/$newPostKey'] = postData;
return FirebaseDatabase.instance.ref().update(updates);
}
يستخدم هذا المثال push() لإنشاء مشاركة في العقدة التي تحتوي على مشاركات لجميع المستخدمين في /posts/$postid واسترداد المفتاح في الوقت نفسه باستخدام key. يمكن بعد ذلك استخدام المفتاح لإنشاء إدخال ثانٍ في مشاركات المستخدم على /user-posts/$userid/$postid.
باستخدام هذه المسارات، يمكنك إجراء تعديلات متزامنة على مواقع متعددة في شجرة JSON من خلال طلب واحد إلى update()، كما هو موضّح في المثال الذي ينشئ المشاركة الجديدة في كلا الموقعين. تكون التعديلات المتزامنة التي يتم إجراؤها بهذه الطريقة كلّية: إما أن تنجح جميع التعديلات أو أن تتعذّر جميعها.
إضافة دالة ردّ عند اكتمال العملية
إذا أردت معرفة الوقت الذي تم فيه حفظ بياناتك، يمكنك تسجيل عمليات رد الاتصال عند اكتمال العملية. تعرض كلّ من set() وupdate() القيمة Future، والتي يمكنك إرفاق عمليات معاودة الاتصال ناجحة وخاطئة بها، ويتم استدعاؤها عند إكمال عملية الكتابة في قاعدة البيانات وعندما تكون المكالمة غير ناجحة.
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
حذف البيانات
أبسط طريقة لحذف البيانات هي استدعاء remove() على مرجع إلى موقع تلك البيانات.
يمكنك أيضًا الحذف عن طريق تحديد قيمة فارغة لعملية كتابة أخرى، مثل set() أو update(). يمكنك استخدام هذه الطريقة مع update() لحذف عدة عناصر فرعية في طلب بيانات من واجهة برمجة التطبيقات واحد.
حفظ البيانات كمعاملات
عند التعامل مع بيانات قد تتلف بسبب تعديلات متزامنة، مثل العدادات المتزايدة، يمكنك استخدام معاملة من خلال تمرير معالج معاملات إلى runTransaction(). يتلقّى معالج المعاملات حالة البيانات الحالية كمعلَمة ويعرض الحالة الجديدة المطلوبة التي تريد كتابتها. إذا كتب عميل آخر إلى الموقع قبل أن تتم كتابة القيمة الجديدة بنجاح، سيتم استدعاء دالة التعديل مرة أخرى باستخدام القيمة الحالية الجديدة، وستتم إعادة محاولة الكتابة.
على سبيل المثال، في تطبيق التدوين الاجتماعي المذكور، يمكنك السماح للمستخدمين بوضع نجمة على المنشورات وإزالتها، وتتبُّع عدد النجوم التي حصل عليها المنشور على النحو التالي:
void toggleStar(String uid) async {
DatabaseReference postRef =
FirebaseDatabase.instance.ref("posts/foo-bar-123");
TransactionResult result = await postRef.runTransaction((Object? post) {
// Ensure a post at the ref exists.
if (post == null) {
return Transaction.abort();
}
Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
if (_post["stars"] is Map && _post["stars"][uid] != null) {
_post["starCount"] = (_post["starCount"] ?? 1) - 1;
_post["stars"][uid] = null;
} else {
_post["starCount"] = (_post["starCount"] ?? 0) + 1;
if (!_post.containsKey("stars")) {
_post["stars"] = {};
}
_post["stars"][uid] = true;
}
// Return the new data.
return Transaction.success(_post);
});
}
بشكلٍ تلقائي، يتم إنشاء الأحداث في كل مرة يتم فيها تشغيل وظيفة تعديل المعاملة،
لذا إذا شغّلت الوظيفة عدة مرات، قد تظهر لك حالات وسيطة.
يمكنك ضبط applyLocally على false لإيقاف هذه الحالات الوسيطة والانتظار إلى حين اكتمال المعاملة قبل إنشاء الأحداث:
await ref.runTransaction((Object? post) {
// ...
}, applyLocally: false);
نتيجة المعاملة هي TransactionResult، والتي تحتوي على معلومات، مثل ما إذا تم تنفيذ المعاملة، واللقطة الجديدة:
DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");
TransactionResult result = await ref.runTransaction((Object? post) {
// ...
});
print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot
إلغاء معاملة
إذا أردت إلغاء معاملة بأمان، اتّصِل بالرقم Transaction.abort() لطرح AbortTransactionException:
TransactionResult result = await ref.runTransaction((Object? user) {
if (user !== null) {
return Transaction.abort();
}
// ...
});
print(result.committed); // false
الزيادات التصاعدية من جهة الخادم
في حالة الاستخدام أعلاه، نكتب قيمتَين في قاعدة البيانات: معرّف المستخدم الذي يضيف نجمة إلى المنشور أو يزيلها منه، وعدد النجوم الذي تمّت زيادته. إذا كنّا نعرف مسبقًا أنّ المستخدم يضع نجمة على المشاركة، يمكننا استخدام عملية زيادة ذرية بدلاً من معاملة.
void addStar(uid, key) async {
Map<String, Object?> updates = {};
updates["posts/$key/stars/$uid"] = true;
updates["posts/$key/starCount"] = ServerValue.increment(1);
updates["user-posts/$key/stars/$uid"] = true;
updates["user-posts/$key/starCount"] = ServerValue.increment(1);
return FirebaseDatabase.instance.ref().update(updates);
}
لا يستخدم هذا الرمز عملية معاملة، لذا لا تتم إعادة تنفيذه تلقائيًا في حال حدوث تعديل متعارض. ومع ذلك، بما أنّ عملية الزيادة تتم مباشرةً على خادم قاعدة البيانات، لا توجد فرصة لحدوث تعارض.
إذا كنت تريد رصد التعارضات الخاصة بالتطبيق ورفضها، مثل أن يضع مستخدم نجمة على مشاركة سبق أن وضع نجمة عليها، عليك كتابة قواعد أمان مخصّصة لحالة الاستخدام هذه.
العمل على البيانات بلا اتصال بالإنترنت
إذا فقد أحد العملاء الاتصال بالشبكة، سيستمر تطبيقك في العمل بشكل صحيح.
يحتفظ كل عميل مرتبط بقاعدة بيانات Firebase بنسخته الداخلية الخاصة من أي بيانات نشطة. عند كتابة البيانات، يتم تسجيلها أولاً في هذه النسخة المحلية. بعد ذلك، يزامن عميل Firebase هذه البيانات مع خوادم قاعدة البيانات البعيدة ومع العملاء الآخرين على أساس "بذل قصارى الجهد".
نتيجةً لذلك، تؤدي جميع عمليات الكتابة إلى قاعدة البيانات إلى تشغيل الأحداث المحلية على الفور، قبل كتابة أي بيانات على الخادم. وهذا يعني أنّ تطبيقك سيظل متجاوبًا بغض النظر عن وقت استجابة الشبكة أو الاتصال بها.
وبعد إعادة إنشاء الاتصال، يتلقّى تطبيقك مجموعة الأحداث المناسبة لكي تتم مزامنة العميل مع حالة الخادم الحالية، بدون الحاجة إلى كتابة أي رمز مخصّص.
سنتحدّث أكثر عن السلوك غير الإلكتروني في مقالة مزيد من المعلومات عن الإمكانات على الإنترنت وبلا اتصال بالإنترنت.