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