خواندن و نوشتن داده‌ها در پلاتفرم‌های 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. فراخوانی از کد پیش‌نمونه برنامه بااستفاده از Realtime Database پلاتفرم کیت توسعه نرم‌افزار طبق معمول، یا بااستفاده از Realtime Database REST API.

راهنمای گام‌به‌گام مفصلی درباره Realtime Database و Cloud Functions دردسترس است. همچنین باید نگاهی به Local Emulator Suite مقدمه بیندازید.

دریافت FIRDatabaseReference

برای خواندن یا نوشتن داده‌ها در پایگاه داده، به نمونه‌ای از FIRDatabaseReference نیاز دارید:

Swift

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
@property (strong, nonatomic) FIRDatabaseReference *ref;

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

نوشتن داده‌ها

این سند اصول اولیه خواندن و نوشتن داده‌های Firebase را پوشش می‌دهد.

داده‌های Firebase در مرجع Database نوشته می‌شود و با پیوست کردن شنونده ناهمزمان به مرجع بازیابی می‌شود. شنونده یک‌بار برای وضعیت اولیه داده‌ها و دوباره هر زمان که داده‌ها تغییر می‌کند راه‌اندازی می‌شود.

عملیات نوشتن پایه

برای عملیات نوشتن پایه، می‌توانید از setValue برای ذخیره کردن داده‌ها در مرجع مشخص‌شده استفاده کنید و داده‌های موجود در آن مسیر را جایگزین کنید. می‌توانید از این روش برای موارد زیر استفاده کنید:

  • انواع مجوز که با انواع JSON دردسترس مطابقت دارند به این صورت است:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

برای مثال، می‌توانید کاربری را با setValue به این صورت اضافه کنید:

Swift

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

استفاده از setValue به این روش داده‌ها را در مکان مشخص‌شده، ازجمله هر گره فرزند، بازنویسی می‌کند. بااین‌حال، همچنان می‌توانید بدون بازنویسی کل شیء، کودک را به‌روز کنید. اگر می‌خواهید به کاربران اجازه دهید نمایه‌هایشان را به‌روز کنند می‌توانید نام کاربری را به این صورت به‌روز کنید:

Swift

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

خواندن داده‌ها

خواندن داده‌ها با گوش دادن به رویدادهای مقدار

برای خواندن داده‌ها در یک مسیر و گوش دادن به تغییرات، از observeEventType:withBlock FIRDatabaseReference برای مشاهده FIRDataEventTypeValue رویداد استفاده کنید.

نوع رویداد کاربرد معمول
FIRDataEventTypeValue تغییرات کل محتوای مسیر را بخواند و به آن‌ها گوش دهد.

می‌توانید از رویداد FIRDataEventTypeValue برای خواندن داده‌ها در مسیر داده‌شده استفاده کنید، همان‌طور که در زمان رویداد وجود دارد. این روش یک‌بار وقتی که شنونده پیوست می‌شود و هر بار که داده‌ها، ازجمله هرگونه فرزند، تغییر می‌کند دوباره راه‌اندازی می‌شود. یک snapshot حاوی همه داده‌های آن مکان، ازجمله داده‌های فرزند، به برگشت رویداد ارسال می‌شود. اگر داده‌ای وجود نداشته باشد، وقتی با exists() تماس می‌گیرید، لحظه‌عکس false را برمی‌گرداند و وقتی دارایی value آن را می‌خوانید، nil را برمی‌گرداند.

مثال زیر نشان می‌دهد که چگونه یک برنامه وبلاگ‌نویسی اجتماعی جزئیات یک پست را از پایگاه داده بازیابی می‌کند:

Swift

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

شنونده FIRDataSnapshot را دریافت می‌کند که حاوی داده‌ها در مکان مشخص‌شده در پایگاه داده در زمان رویداد در دارایی value آن است. می‌توانید مقادیر را به نوع بومی مناسب، مانند NSDictionary، اختصاص دهید. اگر در مکان موردنظر داده‌ای وجود نداشته باشد، value nil است.

یک‌بار داده‌ها را بخواند

یک‌بار بااستفاده از getData() خوانده می‌شود

این کیت توسعه نرم‌افزار برای مدیریت تعاملات با سرورهای پایگاه داده طراحی شده است، چه برنامه شما آنلاین باشد چه آفلاین.

به‌طورکلی، باید از تکنیک‌های رویدادهای مقدار که در بالا توضیح داده شد برای خواندن داده‌ها استفاده کنید تا از به‌روزرسانی‌های داده‌ها از زیرینه مطلع شوید. این تکنیک‌ها استفاده و صورت‌حساب شما را کاهش می‌دهند و بهینه‌سازی شده‌اند تا بهترین تجربه را به کاربران شما در حالت آنلاین و آفلاین ارائه دهند.

اگر فقط یک‌بار به داده‌ها نیاز دارید، می‌توانید از getData() برای دریافت نمای لحظه‌ای از داده‌های پایگاه داده استفاده کنید. اگر به‌هر دلیلی getData() نتواند مقدار سرور را برگرداند، کارخواه حافظه نهان فضای ذخیره‌سازی محلی را کاوش می‌کند و اگر مقدار همچنان پیدا نشد، خطا برمی‌گرداند.

مثال زیر نشان می‌دهد که چگونه نام کاربری عمومی کاربر یک‌بار از پایگاه داده بازیابی می‌شود:

Swift

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

Objective-C

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
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 در هدف «کلیپ برنامه» دردسترس نیست.
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 در هدف «کلیپ برنامه» دردسترس نیست.
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 در هدف «کلیپ برنامه» دردسترس نیست.
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 در هدف «کلیپ برنامه» دردسترس نیست.
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 استفاده کرد.

بااستفاده از این مسیرها، می‌توانید با یک تماس با updateChildValues، به‌طور هم‌زمان چندین مکان را در درخت JSON به‌روز کنید، مثلاً همان‌طور که این مثال پست جدید را در هر دو مکان ایجاد می‌کند. به‌روزرسانی‌های هم‌زمان که به این روش انجام می‌شوند اتمی هستند: یا همه به‌روزرسانی‌ها موفقیت‌آمیز هستند یا همه به‌روزرسانی‌ها ناموفق هستند.

افزودن «بلوک تکمیل»

اگر می‌خواهید بدانید داده‌هایتان چه زمانی ثبت شده است، می‌توانید بلوک تکمیل اضافه کنید. هم setValue و هم updateChildValues یک بلوک اختیاری تکمیل می‌گیرند که وقتی نوشتن در پایگاه داده انجام شد فراخوانی می‌شود. این شنونده می‌تواند برای پیگیری اینکه کدام داده‌ها ذخیره شده‌اند و کدام داده‌ها هنوز درحال همگام‌سازی هستند مفید باشد. اگر تماس ناموفق باشد، شیء خطایی به شنونده ارسال می‌شود که دلیل ناموفق بودن تماس را نشان می‌دهد.

Swift

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
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 در هدف «کلیپ برنامه» دردسترس نیست.
[[[_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 برای حذف چند فرزند در یک تماس API استفاده کنید.

جدا کردن شنوندگان

وقتی ViewController را ترک می‌کنید، همگام‌سازی داده‌ها برای ناظران به‌طور خودکار متوقف نمی‌شود. اگر ناظر به‌درستی برداشته نشود، همچنان داده‌ها را با حافظه محلی همگام‌سازی می‌کند. وقتی دیگر به ناظر نیاز نیست، آن را با ارسال FIRDatabaseHandle مرتبط به روش removeObserverWithHandle بردارید.

وقتی یک بلوک تماس برگشتی به یک مرجع اضافه می‌کنید، یک FIRDatabaseHandle برگردانده می‌شود. از این نام‌های کاربری می‌توان برای برداشتن مسدودسازی تماس برگشتی استفاده کرد.

اگر چندین شنونده به مرجع پایگاه داده اضافه شده باشند، هر شنونده هنگام وقوع رویداد فراخوانده می‌شود. برای متوقف کردن همگام‌سازی داده‌ها در آن مکان، باید همه ناظران را در مکان با فراخوانی روش removeAllObservers بردارید.

تماس با removeObserverWithHandle یا removeAllObservers در یک شنونده به‌طور خودکار شنوندگان ثبت‌شده در گره‌های فرزند آن را حذف نمی‌کند؛ شما باید آن ارجاع‌ها یا دسته‌ها را نیز پیگیری کنید تا آن‌ها را حذف کنید.

ذخیره داده‌ها به‌عنوان تراکنش

هنگام کار با داده‌هایی که ممکن است با تغییرات هم‌زمان خراب شوند، مثل شمارنده‌های افزایشی، می‌توانید از عملکرد تراکنش استفاده کنید. برای این عملیات دو آرگومان ارائه می‌دهید: تابع به‌روزرسانی و کال‌بک اختیاری تکمیل. تابع به‌روزرسانی وضعیت فعلی داده‌ها را به‌عنوان متغیر مستقل می‌گیرد و وضعیت مطلوب جدیدی را که می‌خواهید بنویسید برمی‌گرداند.

برای مثال، در برنامه وبلاگ‌نویسی اجتماعی نمونه، می‌توانید به کاربران اجازه دهید پست‌ها را ستاره‌دار و بدون ستاره کنند و تعداد ستاره‌های دریافتی یک پست را به این صورت پیگیری کنید:

Swift

توجه: این محصول Firebase در هدف «کلیپ برنامه» دردسترس نیست.
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 در هدف «کلیپ برنامه» دردسترس نیست.
[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 در هدف «کلیپ برنامه» دردسترس نیست.
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 در هدف «کلیپ برنامه» دردسترس نیست.
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 آن داده‌ها را با سرورهای پایگاه داده از دور و با کارخواه‌های دیگر براساس «بهترین تلاش» همگام‌سازی می‌کند.

در نتیجه، همه نوشتن‌ها در پایگاه داده بلافاصله رویدادهای محلی را پیش‌از اینکه داده‌ای در سرور نوشته شود راه‌اندازی می‌کنند. این یعنی برنامه شما صرف‌نظر از تأخیر شبکه یا اتصال‌پذیری، همچنان پاسخ‌گو است.

پس‌از برقراری مجدد اتصال، برنامه شما مجموعه مناسبی از رویدادها را دریافت می‌کند تا مشتری با وضعیت فعلی سرور همگام‌سازی شود، بدون اینکه نیاز به نوشتن کد سفارشی داشته باشد.

در بخش درباره قابلیت‌های آنلاین و آفلاین بیشتر بدانید درباره رفتار آفلاین بیشتر صحبت خواهیم کرد.

مراحل بعدی