خواندن و نوشتن داده ها در پلتفرم های اپل

(اختیاری) نمونه اولیه و تست با Firebase Local Emulator Suite

قبل از صحبت در مورد نحوه خواندن و نوشتن برنامه شما از Realtime Database ، بیایید مجموعه‌ای از ابزارهایی را که می‌توانید برای نمونه‌سازی اولیه و آزمایش عملکرد Realtime Database استفاده کنید، معرفی کنیم: Firebase Local Emulator Suite . اگر در حال آزمایش مدل‌های داده مختلف، بهینه‌سازی قوانین امنیتی خود یا تلاش برای یافتن مقرون‌به‌صرفه‌ترین راه برای تعامل با back-end هستید، امکان کار به صورت محلی بدون استقرار سرویس‌های زنده می‌تواند ایده خوبی باشد.

یک شبیه‌ساز Realtime Database بخشی از Local Emulator Suite است که به برنامه شما امکان می‌دهد با محتوا و پیکربندی پایگاه داده شبیه‌سازی شده شما و همچنین منابع پروژه شبیه‌سازی شده (توابع، سایر پایگاه‌های داده و قوانین امنیتی) تعامل داشته باشد.

استفاده از شبیه‌ساز Realtime Database فقط شامل چند مرحله است:

  1. اضافه کردن یک خط کد به فایل پیکربندی آزمایشی برنامه برای اتصال به شبیه‌ساز.
  2. از ریشه دایرکتوری پروژه محلی خود، firebase emulators:start .
  3. طبق معمول، با استفاده از SDK پلتفرم Realtime Database یا با استفاده از API REST Realtime Database ، از کد نمونه اولیه برنامه خود فراخوانی انجام دهید.

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

دریافت یک مرجع پایگاه داده FIR

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

سویفت

توجه: این محصول Firebase در App Clip target در دسترس نیست.
var ref: DatabaseReference!

ref = Database.database().reference()

هدف-سی

توجه: این محصول Firebase در App Clip target در دسترس نیست.
@property (strong, nonatomic) FIRDatabaseReference *ref;

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

نوشتن داده

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

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

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

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

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

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

سویفت

توجه: این محصول Firebase در App Clip target در دسترس نیست.
self.ref.child("users").child(user.uid).setValue(["username": username])

هدف-سی

توجه: این محصول Firebase در App Clip target در دسترس نیست.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

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

سویفت

توجه: این محصول Firebase در App Clip target در دسترس نیست.
self.ref.child("users/\(user.uid)/username").setValue(username)

هدف-سی

توجه: این محصول Firebase در App Clip target در دسترس نیست.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

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

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

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

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

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

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

سویفت

توجه: این محصول Firebase در App Clip target در دسترس نیست.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

هدف-سی

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

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

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

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

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

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

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

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

سویفت

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

هدف-سی

توجه: این محصول Firebase در App Clip target در دسترس نیست.
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 برای دریافت فوری داده‌ها از حافظه پنهان دیسک محلی استفاده کنید.

این برای داده‌هایی مفید است که فقط یک بار نیاز به بارگذاری دارند و انتظار نمی‌رود که مرتباً تغییر کنند یا نیاز به گوش دادن فعال داشته باشند. برای مثال، برنامه وبلاگ نویسی در مثال‌های قبلی از این روش برای بارگذاری پروفایل کاربر هنگام شروع نوشتن یک پست جدید استفاده می‌کند:

سویفت

توجه: این محصول Firebase در App Clip target در دسترس نیست.
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)
}

هدف-سی

توجه: این محصول Firebase در App Clip target در دسترس نیست.
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 ، می‌توانید مقادیر فرزند سطح پایین‌تر را با مشخص کردن مسیری برای کلید به‌روزرسانی کنید. اگر داده‌ها برای مقیاس‌پذیری بهتر در چندین مکان ذخیره می‌شوند، می‌توانید تمام نمونه‌های آن داده‌ها را با استفاده از data fan-out به‌روزرسانی کنید. به عنوان مثال، یک برنامه وبلاگ‌نویسی اجتماعی ممکن است بخواهد پستی ایجاد کند و همزمان آن را در فید فعالیت اخیر و فید فعالیت کاربر ارسال‌کننده به‌روزرسانی کند. برای انجام این کار، برنامه وبلاگ‌نویسی از کدی مانند این استفاده می‌کند:

سویفت

توجه: این محصول Firebase در App Clip target در دسترس نیست.
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)

هدف-سی

توجه: این محصول Firebase در App Clip target در دسترس نیست.
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 ​​یک بلوک تکمیل اختیاری می‌گیرند که وقتی نوشتن در پایگاه داده ثبت شده است، فراخوانی می‌شود. این شنونده می‌تواند برای پیگیری اینکه کدام داده‌ها ذخیره شده‌اند و کدام داده‌ها هنوز در حال همگام‌سازی هستند، مفید باشد. اگر فراخوانی ناموفق باشد، یک شیء خطا به شنونده ارسال می‌شود که دلیل وقوع شکست را نشان می‌دهد.

سویفت

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

هدف-سی

توجه: این محصول Firebase در App Clip target در دسترس نیست.
[[[_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 ترک می‌کنید، Observerها به‌طور خودکار همگام‌سازی داده‌ها را متوقف نمی‌کنند. اگر یک observer به درستی حذف نشود، همچنان به همگام‌سازی داده‌ها با حافظه محلی ادامه می‌دهد. وقتی دیگر به یک observer نیازی نیست، آن را با ارسال FIRDatabaseHandle مرتبط به متد removeObserverWithHandle حذف کنید.

وقتی یک بلوک فراخوانی (callback block) به یک مرجع (reference) اضافه می‌کنید، یک FIRDatabaseHandle برگردانده می‌شود. از این دستگیره‌ها می‌توان برای حذف بلوک فراخوانی استفاده کرد.

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

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

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

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

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

سویفت

توجه: این محصول Firebase در App Clip target در دسترس نیست.
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)
  }
}

هدف-سی

توجه: این محصول Firebase در App Clip target در دسترس نیست.
[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 است. سرور مقدار اولیه را با مقدار فعلی آن مقایسه می‌کند و در صورت مطابقت مقادیر، تراکنش را می‌پذیرد یا آن را رد می‌کند. اگر تراکنش رد شود، سرور مقدار فعلی را به کلاینت برمی‌گرداند که دوباره تراکنش را با مقدار به‌روزرسانی شده اجرا می‌کند. این کار تا زمانی که تراکنش پذیرفته شود یا تلاش‌های زیادی انجام شود، تکرار می‌شود.

افزایش‌های اتمی سمت سرور

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

سویفت

توجه: این محصول Firebase در App Clip target در دسترس نیست.
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)

هدف-سی

توجه: این محصول Firebase در App Clip target در دسترس نیست.
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 آن داده‌ها را با سرورهای پایگاه داده راه دور و با سایر کلاینت‌ها بر اساس "بهترین تلاش" همگام‌سازی می‌کند.

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

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

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

مراحل بعدی