قبل از صحبت در مورد نحوه خواندن و نوشتن برنامه شما از Realtime Database ، بیایید مجموعهای از ابزارهایی را که میتوانید برای نمونهسازی و آزمایش عملکرد Realtime Database استفاده کنید، معرفی میکنیم: Firebase Local Emulator Suite . اگر در حال آزمایش مدل های مختلف داده، بهینه سازی قوانین امنیتی خود هستید یا برای یافتن مقرون به صرفه ترین راه برای تعامل با back-end تلاش می کنید، اینکه بتوانید به صورت محلی بدون استقرار سرویس های زنده کار کنید می تواند ایده خوبی باشد.
شبیه ساز Realtime Database بخشی از Local Emulator Suite است که به برنامه شما امکان می دهد با محتوای پایگاه داده شبیه سازی شده و پیکربندی شما و همچنین به صورت اختیاری منابع پروژه شبیه سازی شده شما (توابع، سایر پایگاه های داده و قوانین امنیتی) تعامل داشته باشد.
استفاده از شبیه ساز Realtime Database فقط شامل چند مرحله است:
- افزودن یک خط کد به پیکربندی آزمایشی برنامه برای اتصال به شبیه ساز.
- از ریشه دایرکتوری پروژه محلی خود،
firebase emulators:start
اجرا کنید. - برقراری تماس از کد نمونه اولیه برنامه خود با استفاده از یک SDK پلتفرم Realtime Database به طور معمول، یا با استفاده از Realtime Database REST API.
یک بررسی دقیق شامل Realtime Database و Cloud Functions در دسترس است. همچنین باید نگاهی به معرفی Local Emulator Suite داشته باشید.
برای خواندن یا نوشتن داده ها از پایگاه داده، به یک نمونه از FIRDatabaseReference
نیاز دارید:
var ref: DatabaseReference! ref = Database.database().reference()
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
این سند اصول خواندن و نوشتن داده های Firebase را پوشش می دهد.
داده های 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
of FIRDatabaseReference
برای مشاهده رویدادهای FIRDataEventTypeValue
استفاده کنید.
نوع رویداد | استفاده معمولی |
---|---|
FIRDataEventTypeValue | برای تغییرات در کل محتوای یک مسیر بخوانید و گوش دهید. |
میتوانید از رویداد FIRDataEventTypeValue
برای خواندن دادهها در یک مسیر مشخص استفاده کنید، همانطور که در زمان رویداد وجود دارد. این روش یک بار در زمانی که شنونده متصل می شود و دوباره هر بار که داده ها، از جمله کودکان، تغییر می کنند، فعال می شود. پاسخ تماس رویداد یک snapshot
حاوی تمام دادهها در آن مکان، از جمله دادههای فرزند ارسال میشود. اگر داده ای وجود نداشته باشد، عکس فوری با فراخوانی exists()
exizes 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
آن است. می توانید مقادیر را به نوع بومی مناسب، مانند NSDictionary
اختصاص دهید. اگر هیچ داده ای در مکان وجود نداشته باشد، value
nil
است.
SDK برای مدیریت تعاملات با سرورهای پایگاه داده، چه برنامه شما آنلاین یا آفلاین باشد، طراحی شده است.
به طور کلی، باید از تکنیکهای رویدادهای ارزشی که در بالا توضیح داده شد برای خواندن دادهها استفاده کنید تا از بهروزرسانیهای دادهها از باطن مطلع شوید. این تکنیکها استفاده و صورتحساب شما را کاهش میدهند، و بهینهسازی شدهاند تا بهترین تجربه را هنگام آنلاین شدن و آفلاین شدن کاربرانتان ارائه دهند.
اگر فقط یک بار به داده ها نیاز دارید، می توانید 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
، میتوانید مقادیر فرزند سطح پایینتر را با تعیین مسیری برای کلید بهروزرسانی کنید. اگر دادهها در مکانهای مختلف ذخیره میشوند تا مقیاس بهتری داشته باشند، میتوانید تمام نمونههای آن دادهها را با استفاده از خروجی فنآوری داده بهروزرسانی کنید. به عنوان مثال، یک برنامه وبلاگ نویسی اجتماعی ممکن است بخواهد یک پست ایجاد کند و به طور همزمان آن را به فید فعالیت اخیر و فید فعالیت کاربر پست کننده به روز کند. برای انجام این کار، برنامه وبلاگ نویسی از کد زیر استفاده می کند:
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
خارج میشوید، مشاهدهکنندگان بهطور خودکار همگامسازی دادهها را متوقف نمیکنند. اگر یک ناظر به درستی حذف نشود، به همگام سازی داده ها با حافظه محلی ادامه می دهد. وقتی دیگر نیازی به مشاهدهگر نیست، با ارسال FIRDatabaseHandle
مرتبط به متد removeObserverWithHandle
آن را حذف کنید.
هنگامی که یک بلوک پاسخ به تماس را به یک مرجع اضافه می کنید، یک FIRDatabaseHandle
برمی گردد. از این دسته ها می توان برای حذف بلوک برگشت به تماس استفاده کرد.
اگر چندین شنونده به یک مرجع پایگاه داده اضافه شده باشد، هر شنونده زمانی فراخوانی می شود که رویدادی مطرح شود. به منظور توقف همگام سازی داده ها در آن مکان، باید با فراخوانی روش removeAllObservers
، همه ناظران را در یک مکان حذف کنید.
فراخوانی 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 آن داده ها را با سرورهای پایگاه داده راه دور و با سایر مشتریان بر اساس "بهترین تلاش" همگام سازی می کند.
در نتیجه، همه نوشتهها در پایگاه داده، بلافاصله رویدادهای محلی را راهاندازی میکنند، قبل از اینکه دادهای روی سرور نوشته شود. این بدان معناست که برنامه شما بدون توجه به تأخیر شبکه یا اتصال، پاسخگو باقی می ماند.
پس از برقراری مجدد اتصال، برنامه شما مجموعه مناسبی از رویدادها را دریافت می کند تا کلاینت بدون نیاز به نوشتن کد سفارشی با وضعیت سرور فعلی همگام شود.
در مورد قابلیت های آنلاین و آفلاین بیشتر بدانید در مورد رفتار آفلاین بیشتر صحبت خواهیم کرد.