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

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

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

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

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

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

تتوفّر جولة تفصيلية تشمل قاعدة بيانات "الوقت الفعلي" وCloud Functions. ومن المفترض أيضًا أن تُلقي نظرة على مقدمة حول مجموعة أدوات المحاكاة المحلية.

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

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

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

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

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