(اختیاری) نمونهسازی و آزمایش با Firebase Local Emulator Suite
قبلاز صحبت درباره نحوه خواندن و نوشتن برنامهتان در Realtime Database، مجموعهای از ابزارها را معرفی میکنیم که میتوانید برای نمونهسازی و آزمایش عملکرد Realtime Database استفاده کنید: Firebase Local Emulator Suite. اگر درحال امتحان کردن مدلهای داده مختلف، بهینهسازی قوانین امنیتی، یا تلاش برای یافتن مقرونبهصرفهترین راه برای تعامل با پسبک هستید، توانایی کار کردن بهصورت محلی بدون استقرار خدمات زنده میتواند ایده بسیار خوبی باشد.
شبیهساز Realtime Database بخشی از Local Emulator Suite است که به برنامه شما امکان میدهد با محتوا و پیکربندی پایگاه داده شبیهسازیشده شما، و همچنین بهصورت اختیاری با منابع پروژه شبیهسازیشده شما (توابع، پایگاههای داده دیگر، و قوانین امنیتی) تعامل داشته باشد.
استفاده از شبیهساز Realtime Database فقط چند مرحله دارد:
- افزودن یک خط کد به پیکربندی آزمایشی برنامهتان برای اتصال به شبیهساز.
- از ریشه دایرکتوری پروژه محلی شما، درحال اجرای
firebase emulators:start. - برقراری تماس از کد نمونه اولیه برنامه بااستفاده از کیت توسعه نرمافزار پلاتفرم Realtime Database طبق معمول، یا بااستفاده از REST API Realtime Database.
راهنمای گامبهگام دقیقی شامل Realtime Database و Cloud Functions دردسترس است. همچنین باید نگاهی به Local Emulator Suite مقدمه بیندازید.
دریافت FIRDatabaseReference
برای خواندن یا نوشتن دادهها از پایگاه داده، به نمونهای از
FIRDatabaseReference نیاز دارید:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
نوشتن داده
این سند اصول اولیه خواندن و نوشتن دادههای Firebase را پوشش میدهد.
دادههای Firebase در مرجع Database نوشته میشود و با پیوست کردن شنونده ناهمگام به مرجع بازیابی میشود. شنونده یکبار برای وضعیت اولیه دادهها و دوباره هرزمان که دادهها تغییر کند راهاندازی میشود.
عملیات نوشتن پایه
برای عملیات نوشتن پایه، میتوانید از setValue برای ذخیره دادهها در یک مرجع مشخص استفاده کنید و هر داده موجود در آن مسیر را جایگزین کنید. میتوانید از این روش برای موارد زیر استفاده کنید:
- انواع مجوز که با انواع JSON دردسترس بهصورت زیر مطابقت دارند:
NSStringNSNumberNSDictionaryNSArray
برای مثال، میتوانید کاربری را با setValue بهصورت زیر اضافه کنید:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
استفاده از setValue به این روش، دادهها را در مکان مشخصشده،
ازجمله گرههای فرزند، بازنویسی میکند. بااینحال، همچنان میتوانید فرزند را بدون بازنویسی کل شیء بهروز کنید. اگر میخواهید به کاربران اجازه دهید نمایههایشان را بهروز کنند
میتوانید نام کاربری را بهصورت زیر بهروز کنید:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
دادهها خوانده شد
خواندن دادهها با گوش دادن به رویدادهای مقدار
برای خواندن دادهها در یک مسیر و گوش دادن به تغییرات، از
observeEventType:withBlock از FIRDatabaseReference برای مشاهده
رویدادهای FIRDataEventTypeValue استفاده کنید.
| نوع رویداد | استفاده معمول |
|---|---|
FIRDataEventTypeValue |
خواندن و گوش دادن به تغییرات در کل محتوای یک مسیر. |
میتوانید از رویداد FIRDataEventTypeValue برای خواندن دادهها در مسیر مشخصشده استفاده کنید،
همانطور که در زمان رویداد وجود دارد. این روش یکبار هنگام پیوست شدن شنونده و بار دیگر هربار که دادهها، ازجمله هرگونه فرزند، تغییر میکند، راهاندازی میشود. فراخوانی رویداد به snapshot منتقل میشود که شامل همه دادهها در آن مکان، ازجمله دادههای فرزند است. اگر دادهای وجود نداشته باشد، عکس آنی
false را هنگام تماس با exists() و nil را هنگام خواندن ویژگی value آن برمیگرداند.
مثال زیر نشان میدهد که چگونه یک برنامه وبلاگنویسی اجتماعی جزئیات یک پست را از پایگاه داده بازیابی میکند:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
شنونده یک FIRDataSnapshot دریافت میکند که حاوی دادهها در مکان مشخصشده در پایگاه داده در زمان رویداد در ویژگی value آن است. میتوانید
مقادیر را به نوع بومی مناسب، مانند NSDictionary، اختصاص دهید.
اگر دادهای در مکان وجود نداشته باشد، value برابر با nil است.
خواندن دادهها برای یکبار
یکبار خواندن بااستفاده از getData()
این کیت توسعه نرمافزار برای مدیریت تعاملات با سرورهای پایگاه داده طراحی شده است، چه برنامه شما آنلاین باشد و چه آفلاین.
بهطور کلی، باید از تکنیکهای رویدادهای مقدار که در بالا شرح داده شده است برای خواندن دادهها استفاده کنید تا از بهروزرسانیهای دادهها از زیرینه مطلع شوید. این روشها استفاده و صورتحساب شما را کاهش میدهند و برای ارائه بهترین تجربه به کاربران شما هنگام آنلاین و آفلاین شدن بهینهسازی شدهاند.
اگر فقط یکبار به دادهها نیاز دارید، میتوانید از getData() برای دریافت عکس آنی از
دادههای پایگاه داده استفاده کنید. اگر به هر دلیلی getData() نتواند مقدار سرور را برگرداند، کارخواه حافظه نهان فضای ذخیرهسازی محلی را کاوش میکند و اگر مقدار همچنان پیدا نشود، خطایی برمیگرداند.
مثال زیر بازیابی نام کاربری عمومی کاربر را یکبار از پایگاه داده نشان میدهد:
Swift
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
Objective-C
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
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
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
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
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
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
[[[_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
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
[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
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
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 آن دادهها را با سرورهای پایگاه داده راه دور و با کارخواههای دیگر براساس «بهترین تلاش» همگامسازی میکند.
درنتیجه، همه نوشتنها در پایگاه داده بلافاصله رویدادهای محلی را راهاندازی میکنند، قبلاز اینکه دادهای در سرور نوشته شود. این یعنی برنامهتان بدون توجه به تأخیر شبکه یا اتصال، واکنشگرا باقی میماند.
پساز برقراری مجدد اتصال، برنامه شما مجموعه مناسبی از رویدادها را دریافت میکند تا سرویسگیرنده با وضعیت فعلی سرور همگامسازی شود، بدون اینکه نیازی به نوشتن کد سفارشی باشد.
در درباره قابلیتهای آنلاین و آفلاین بیشتر بدانید، درباره رفتار آفلاین بیشتر صحبت خواهیم کرد.