قراءة البيانات وكتابتها على منصات 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 الأساسي كالمعتاد، أو باستخدام واجهة برمجة التطبيقات Realtime Database REST API

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

الحصول على عنصر FIRDatabaseReference

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

Swift

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

ref = Database.database().reference()

Objective-C

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

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

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

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

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

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

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

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

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

Swift

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

Objective-C

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

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

Swift

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

Objective-C

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

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

قراءة البيانات من خلال رصد أحداث القيم

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

نوع الحدث الاستخدام المعتاد
FIRDataEventTypeValue قراءة التغييرات في محتوى مسار معيّن والاستماع إليها

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

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

Swift

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

Objective-C

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

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

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

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

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

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

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

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

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في استهداف تطبيق 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 هذا في استهداف تطبيق 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 هذا في استهداف تطبيق 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 هذا في استهداف تطبيق 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 هذا في استهداف تطبيق 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 هذا في استهداف تطبيق 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 كتلة اكتمال اختيارية يتم استدعاؤها عند تأكيد عملية الكتابة في قاعدة بيانات MySQL. يمكن أن يكون هذا المستمع مفيدًا لتتبُّع البيانات التي تم حفظها والبيانات التي لا تزال تتم مزامنتها. إذا لم تنجح المكالمة، يتم تمرير كائن خطأ إلى المستمع يشير إلى سبب حدوث الخطأ.

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في استهداف تطبيق 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 هذا في استهداف تطبيق 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. في حال عدم إزالة مراقب بشكل صحيح، سيستمر في مزامنة data مع الذاكرة المحلية. عندما لا يعود هناك حاجة إلى مراقب، أزِله من خلال تمرير FIRDatabaseHandle المرتبط إلى طريقة removeObserverWithHandle.

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

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

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

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

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

على سبيل المثال، في مثال تطبيق التدوين الاجتماعي، يمكنك السماح للمستخدمين بتسجيل المحتوى المميّز وعدم تسجيله وتتبُّع عدد النجوم التي حصل عليها المحتوى، وذلك على النحو التالي:

Swift

ملاحظة: لا يتوفّر منتج Firebase هذا في استهداف تطبيق 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 هذا في استهداف تطبيق 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 هذا في استهداف تطبيق 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 هذا في استهداف تطبيق 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 هذه البيانات مع قاعدة بيانات المشغّلات البعيدة ومع برامج أخرى على أساس "أقصى جهد".

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

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

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

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