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

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

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

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

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