قراءة البيانات وكتابتها على منصات Apple

(اختياري) إنشاء النماذج الأولية واختبارها باستخدام Firebase Local Emulator Suite

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

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

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

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

يتوفّر دليل تفصيلي يتضمّن Realtime Database وCloud Functions. ننصحك أيضًا بالاطّلاع على مقدمة حول Local Emulator Suite.

الحصول على FIRDatabaseReference

لقراءة البيانات من قاعدة البيانات أو كتابتها، تحتاج إلى مثيل من FIRDatabaseReference:

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

كتابة البيانات

يتناول هذا المستند أساسيات قراءة بيانات Firebase وكتابتها.

تتم كتابة بيانات Firebase في مرجع Database ويتم استردادها من خلال إرفاق مستمع غير متزامن بالمرجع. يتم تشغيل المستمع مرة واحدة للحالة الأولية للبيانات ومرة أخرى في أي وقت تتغير فيه البيانات.

عمليات الكتابة الأساسية

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

  • أنواع البطاقات التي تتوافق مع أنواع JSON المتاحة على النحو التالي:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

على سبيل المثال، يمكنك إضافة مستخدم باستخدام setValue على النحو التالي:

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

باستخدام setValue بهذه الطريقة، يتم استبدال البيانات في الموقع المحدّد، بما في ذلك أي عُقد فرعية. ومع ذلك، لا يزال بإمكانك تحديث عنصر فرعي دون إعادة كتابة الكائن بأكمله. إذا أردت السماح للمستخدمين بتعديل ملفاتهم الشخصية، يمكنك تعديل اسم المستخدم على النحو التالي:

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

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

قراءة البيانات من خلال الاستماع إلى أحداث القيمة

لقراءة البيانات في مسار معيّن والاستماع إلى التغييرات، استخدِم observeEventType:withBlock من FIRDatabaseReference لمراقبة أحداث FIRDataEventTypeValue.

نوع الحدث الاستخدام المعتاد
FIRDataEventTypeValue قراءة التغييرات التي تطرأ على محتوى مسار بالكامل والاستماع إليها

يمكنك استخدام الحدث FIRDataEventTypeValue لقراءة البيانات في مسار معيّن، كما هي في وقت الحدث. يتم تشغيل هذه الطريقة مرة واحدة عند إرفاق المستمع ومرة أخرى في كل مرة تتغير فيها البيانات، بما في ذلك أي عناصر فرعية. يتم تمرير استدعاء الحدث إلى snapshot يحتوي على جميع البيانات في ذلك الموقع، بما في ذلك البيانات الفرعية. في حال عدم توفّر بيانات، ستعرض اللقطة false عند استدعاء exists() وnil عند قراءة السمة value.

يوضّح المثال التالي تطبيق تدوين اجتماعي يستردّ تفاصيل منشور من قاعدة البيانات:

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

يتلقّى المستمع FIRDataSnapshot يحتوي على البيانات في الموقع المحدّد في قاعدة البيانات في وقت الحدث في السمة value. يمكنك تعيين القيم إلى النوع الأصلي المناسب، مثل NSDictionary. إذا لم تكن هناك بيانات في الموقع الجغرافي، تكون قيمة value هي nil.

قراءة البيانات لمرة واحدة

القراءة مرة واحدة باستخدام getData()‎

تم تصميم حزمة تطوير البرامج (SDK) لإدارة التفاعلات مع خوادم قواعد البيانات سواء كان تطبيقك متصلاً بالإنترنت أو غير متصل بالإنترنت.

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

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

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

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid];
[[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) {
  if (error) {
    NSLog(@"Received an error %@", error);
    return;
  }
  NSString *userName = snapshot.value;
}];

يمكن أن يؤدي الاستخدام غير الضروري لـ getData() إلى زيادة استخدام معدل نقل البيانات وانخفاض الأداء، وهو ما يمكن منعه باستخدام أداة استماع في الوقت الفعلي كما هو موضّح أعلاه.

قراءة البيانات مرة واحدة باستخدام مراقب

في بعض الحالات، قد تحتاج إلى عرض القيمة من ذاكرة التخزين المؤقت المحلية على الفور، بدلاً من البحث عن قيمة معدَّلة على الخادم. في هذه الحالات، يمكنك استخدام observeSingleEventOfType للحصول على البيانات من ذاكرة التخزين المؤقت للقرص المحلي على الفور.

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

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
let userID = Auth.auth().currentUser?.uid
ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in
  // Get user value
  let value = snapshot.value as? NSDictionary
  let username = value?["username"] as? String ?? ""
  let user = User(username: username)

  // ...
}) { error in
  print(error.localizedDescription)
}

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
NSString *userID = [FIRAuth auth].currentUser.uid;
[[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  // Get user value
  User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]];

  // ...
} withCancelBlock:^(NSError * _Nonnull error) {
  NSLog(@"%@", error.localizedDescription);
}];

تعديل البيانات أو حذفها

تعديل حقول معيّنة

للكتابة في الوقت نفسه إلى عناصر فرعية محدّدة لعقدة بدون استبدال العقد الفرعية الأخرى، استخدِم طريقة updateChildValues.

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

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
guard let key = ref.child("posts").childByAutoId().key else { return }
let post = ["uid": userID,
            "author": username,
            "title": title,
            "body": body]
let childUpdates = ["/posts/\(key)": post,
                    "/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
NSString *key = [[_ref child:@"posts"] childByAutoId].key;
NSDictionary *post = @{@"uid": userID,
                       @"author": username,
                       @"title": title,
                       @"body": body};
NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post,
                               [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post};
[_ref updateChildValues:childUpdates];

يستخدم هذا المثال childByAutoId لإنشاء منشور في العقدة التي تحتوي على منشورات لجميع المستخدمين في /posts/$postid واسترداد المفتاح في الوقت نفسه باستخدام getKey(). يمكن بعد ذلك استخدام المفتاح لإنشاء إدخال ثانٍ في مشاركات المستخدم على /user-posts/$userid/$postid.

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

إضافة مجموعة إكمال

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

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
do {
  try await ref.child("users").child(user.uid).setValue(["username": username])
  print("Data saved successfully!")
} catch {
  print("Data could not be saved: \(error).")
}

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
  if (error) {
    NSLog(@"Data could not be saved: %@", error);
  } else {
    NSLog(@"Data saved successfully.");
  }
}];

حذف البيانات

أبسط طريقة لحذف البيانات هي استدعاء removeValue على مرجع إلى موقع تلك البيانات.

يمكنك أيضًا الحذف من خلال تحديد nil كقيمة لعملية كتابة أخرى، مثل setValue أو updateChildValues. يمكنك استخدام هذه الطريقة مع updateChildValues لحذف عدة عناصر فرعية في طلب واحد من واجهة برمجة التطبيقات.

فصل المستمعين

لا يتوقف المراقبون تلقائيًا عن مزامنة البيانات عند مغادرة ViewController. إذا لم تتم إزالة المراقب بشكل صحيح، سيستمر في مزامنة البيانات مع الذاكرة المحلية. عندما لا تكون هناك حاجة إلى المراقب، يمكنك إزالته من خلال تمرير FIRDatabaseHandle المرتبط إلى طريقة removeObserverWithHandle.

عند إضافة مجموعة ردّ اتصال إلى مرجع، يتم عرض FIRDatabaseHandle. يمكن استخدام هذه المقابض لإزالة مجموعة عمليات الاستدعاء.

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

لا يؤدي استدعاء removeObserverWithHandle أو removeAllObservers على مستمع إلى إزالة المستمعين المسجلين على العُقد الفرعية تلقائيًا، بل يجب أيضًا تتبُّع هذه المراجع أو المعالجات لإزالتها.

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

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

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

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
  if var post = currentData.value as? [String: AnyObject],
    let uid = Auth.auth().currentUser?.uid {
    var stars: [String: Bool]
    stars = post["stars"] as? [String: Bool] ?? [:]
    var starCount = post["starCount"] as? Int ?? 0
    if let _ = stars[uid] {
      // Unstar the post and remove self from stars
      starCount -= 1
      stars.removeValue(forKey: uid)
    } else {
      // Star the post and add self to stars
      starCount += 1
      stars[uid] = true
    }
    post["starCount"] = starCount as AnyObject?
    post["stars"] = stars as AnyObject?

    // Set value and report transaction success
    currentData.value = post

    return TransactionResult.success(withValue: currentData)
  }
  return TransactionResult.success(withValue: currentData)
}) { error, committed, snapshot in
  if let error = error {
    print(error.localizedDescription)
  }
}

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) {
  NSMutableDictionary *post = currentData.value;
  if (!post || [post isEqual:[NSNull null]]) {
    return [FIRTransactionResult successWithValue:currentData];
  }

  NSMutableDictionary *stars = post[@"stars"];
  if (!stars) {
    stars = [[NSMutableDictionary alloc] initWithCapacity:1];
  }
  NSString *uid = [FIRAuth auth].currentUser.uid;
  int starCount = [post[@"starCount"] intValue];
  if (stars[uid]) {
    // Unstar the post and remove self from stars
    starCount--;
    [stars removeObjectForKey:uid];
  } else {
    // Star the post and add self to stars
    starCount++;
    stars[uid] = @YES;
  }
  post[@"stars"] = stars;
  post[@"starCount"] = @(starCount);

  // Set value and report transaction success
  currentData.value = post;
  return [FIRTransactionResult successWithValue:currentData];
} andCompletionBlock:^(NSError * _Nullable error,
                       BOOL committed,
                       FIRDataSnapshot * _Nullable snapshot) {
  // Transaction completed
  if (error) {
    NSLog(@"%@", error.localizedDescription);
  }
}];

يؤدي استخدام معاملة إلى منع احتساب عدد النجوم بشكل غير صحيح إذا قام عدة مستخدمين بتمييز المنشور نفسه بنجمة في الوقت نفسه أو إذا كان لدى العميل بيانات قديمة. تكون القيمة المضمّنة في الفئة FIRMutableData في البداية هي آخر قيمة معروفة للعميل للمسار، أو nil في حال عدم توفّر أي قيمة. يقارن الخادم القيمة الأولية بقيمتها الحالية ويقبل المعاملة إذا تطابقت القيم، أو يرفضها. إذا تم رفض المعاملة، يعرض الخادم القيمة الحالية للعميل، الذي يشغّل المعاملة مرة أخرى بالقيمة المعدَّلة. ويتكرر ذلك إلى أن يتم قبول المعاملة أو يتم إجراء عدد كبير جدًا من المحاولات.

الزيادات التصاعدية البسيطة من جهة الخادم

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

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
let updates = [
  "posts/\(postID)/stars/\(userID)": true,
  "posts/\(postID)/starCount": ServerValue.increment(1),
  "user-posts/\(postID)/stars/\(userID)": true,
  "user-posts/\(postID)/starCount": ServerValue.increment(1)
] as [String : Any]
Database.database().reference().updateChildValues(updates)

Objective-C

ملاحظة: لا يتوفّر منتج Firebase هذا في هدف App Clip.
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1],
                        [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]};
[[[FIRDatabase database] reference] updateChildValues:updates];

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

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

العمل على البيانات بلا اتصال بالإنترنت

إذا فقد أحد العملاء اتصاله بالشبكة، سيستمر تطبيقك في العمل بشكل صحيح.

يحتفظ كل عميل متصل بقاعدة بيانات Firebase بنسخته الداخلية الخاصة من أي بيانات نشطة. عند كتابة البيانات، تتم كتابتها في هذه النسخة المحلية أولاً. بعد ذلك، يُزامن عميل Firebase هذه البيانات مع خوادم قواعد البيانات البعيدة ومع العملاء الآخرين على أساس "بذل قصارى الجهد".

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

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

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

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