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

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

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

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

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

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

تتوفر إرشادات تفصيلية تتضمن قاعدة بيانات Realtime ووظائف السحابة . يجب عليك أيضًا إلقاء نظرة على مقدمة 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() ‎ للإشارة إلى موقع تلك البيانات.

يمكنك أيضًا الحذف عن طريق تحديد null كقيمة لعملية كتابة أخرى مثل set() أو update() . يمكنك استخدام هذه التقنية مع update() لحذف عدة توابع في استدعاء واحد لواجهة برمجة التطبيقات (API).

حفظ البيانات كمعاملات

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

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

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

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

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