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

(اختياري) النموذج الأولي والاختبار باستخدام Firebase Emulator Suite

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

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

يتضمن استخدام محاكي Realtime Database بضع خطوات فقط:

  1. إضافة سطر من التعليمات البرمجية إلى التكوين التجريبي لتطبيقك للاتصال بالمحاكي.
  2. من جذر دليل المشروع المحلي ، قم بتشغيل firebase emulators:start .
  3. إجراء مكالمات من رمز النموذج الأولي لتطبيقك باستخدام SDK للنظام الأساسي لقاعدة البيانات Realtime كالمعتاد ، أو باستخدام Realtime Database REST API.

تتوفر إرشادات تفصيلية تتضمن Realtime Database و Cloud Functions . يجب عليك أيضًا إلقاء نظرة على مقدمة Emulator Suite .

احصل على مرجع قاعدة البيانات

لقراءة البيانات أو كتابتها من قاعدة البيانات ، تحتاج إلى مثيل 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 name, leave the age 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 s.

يمكنك استخدام 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() للحصول على البيانات من ذاكرة التخزين المؤقت على القرص المحلي على الفور.

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

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 s ، والتي يمكنك إرفاق عمليات رد نداء النجاح والخطأ التي يتم استدعاؤها عندما يتم الالتزام بالكتابة في قاعدة البيانات وعندما تكون المكالمة غير ناجحة.

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

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

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

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

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