قراءة البيانات وكتابتها

(اختياري) إنشاء نموذج أولي واختباره باستخدام "مجموعة أدوات المحاكاة المحلية لمنصة Firebase"

قبل التحدّث عن كيفية قراءة تطبيقك من قاعدة بيانات الوقت الفعلي والكتابة إليه، لنقدّم مجموعة من الأدوات التي يمكنك استخدامها لإنشاء نموذج أوّلي لوظائف قاعدة بيانات الوقت الفعلي واختبارها: مجموعة أدوات المحاكاة. إذا كنت تجرب نماذج بيانات مختلفة أو تعمل على تحسين قواعد الأمان أو تبحث عن الطريقة الأكثر فعالية من حيث التكلفة للتفاعل مع النظام الخلفي، يمكن أن يكون العمل محليًا بدون نشر الخدمات المباشرة فكرة رائعة.

يُعدّ برنامج محاكاة Realtime Database جزءًا من "مجموعة أدوات المحاكاة المحلية"، ما يتيح لتطبيقك التفاعل مع محتوى قاعدة البيانات المحاكاة وإعداداتها، بالإضافة إلى موارد مشروعك المحاكاة (الدوال وقواعد البيانات الأخرى وقواعد الأمان) بشكل اختياري.emulator_suite_short

لا يتطلّب استخدام برنامج محاكاة قاعدة بيانات الوقت الفعلي سوى بضع خطوات:

  1. إضافة سطر من الرمز البرمجي إلى إعدادات الاختبار في تطبيقك للاتصال ببرنامج المحاكاة
  2. تشغيل firebase emulators:start من جذر دليل مشروع على جهاز المستخدم
  3. إجراء طلبات من رمز النموذج الأولي لتطبيقك باستخدام حزمة تطوير البرامج (SDK) لمنصة قاعدة بيانات الوقت الفعلي كالمعتاد، أو باستخدام Realtime Database 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 للاستماع إلى DatabaseEvents.

يمكنك استخدام 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 هذه البيانات مع خوادم قاعدة البيانات البعيدة ومع العملاء الآخرين على أساس "بذل أفضل جهد".

نتيجةً لذلك، تؤدي جميع عمليات الكتابة في قاعدة البيانات إلى تشغيل الأحداث المحلية على الفور، قبل كتابة أي بيانات على الخادم. يعني ذلك أنّ تطبيقك يظل سريع الاستجابة بغض النظر عن وقت استجابة الشبكة أو الاتصال بها.

بمجرد استعادة الاتصال، يتلقّى تطبيقك المجموعة المناسبة من الأحداث حتى تتم مزامنة العميل مع حالة الخادم الحالية، بدون الحاجة إلى كتابة أي رمز برمجي مخصّص.

سنتحدّث أكثر عن السلوك بلا اتصال بالإنترنت في مزيد من المعلومات عن الإمكانات على الإنترنت وبلا اتصال بالإنترنت.

الخطوات التالية