(Không bắt buộc) Tạo nguyên mẫu và thử nghiệm bằng Firebase Local Emulator Suite
Trước khi nói về cách ứng dụng của bạn đọc và ghi vào Realtime Database, hãy cùng tìm hiểu một bộ công cụ mà bạn có thể dùng để tạo nguyên mẫu và kiểm thử chức năng của Realtime Database: Firebase Local Emulator Suite. Nếu bạn đang thử các mô hình dữ liệu khác nhau, tối ưu hoá quy tắc bảo mật hoặc tìm cách tương tác hiệu quả nhất về chi phí với phần phụ trợ, thì việc có thể làm việc cục bộ mà không cần triển khai dịch vụ trực tiếp có thể là một ý tưởng hay.
Trình mô phỏng Realtime Database là một phần của Local Emulator Suite, cho phép ứng dụng của bạn tương tác với nội dung và cấu hình cơ sở dữ liệu được mô phỏng, cũng như các tài nguyên dự án được mô phỏng (hàm, cơ sở dữ liệu khác và quy tắc bảo mật).
Bạn chỉ cần thực hiện vài bước để sử dụng trình mô phỏng Realtime Database:
- Thêm một dòng mã vào cấu hình kiểm thử của ứng dụng để kết nối với trình mô phỏng.
- Chạy
firebase emulators:starttừ thư mục gốc của thư mục dự án cục bộ. - Thực hiện lệnh gọi từ mã nguyên mẫu của ứng dụng bằng SDK nền tảng Realtime Database như bình thường hoặc sử dụng API REST Realtime Database.
Bạn có thể xem hướng dẫn chi tiết về Realtime Database và Cloud Functions. Bạn cũng nên xem phần giới thiệu về Local Emulator Suite.
Lấy FIRDatabaseReference
Để đọc hoặc ghi dữ liệu từ cơ sở dữ liệu, bạn cần có một thực thể của FIRDatabaseReference:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Ghi dữ liệu
Tài liệu này trình bày những kiến thức cơ bản về cách đọc và ghi dữ liệu Firebase.
Dữ liệu Firebase được ghi vào một tệp đối chiếu Database và được truy xuất bằng cách đính kèm một trình nghe không đồng bộ vào tệp đối chiếu đó. Trình nghe được kích hoạt một lần cho trạng thái ban đầu của dữ liệu và một lần nữa bất cứ khi nào dữ liệu thay đổi.
Thao tác ghi cơ bản
Đối với các thao tác ghi cơ bản, bạn có thể sử dụng setValue để lưu dữ liệu vào một tệp đối chiếu được chỉ định, thay thế mọi dữ liệu hiện có tại đường dẫn đó. Bạn có thể sử dụng phương thức này để:
- Truyền các loại tương ứng với các loại JSON có sẵn như sau:
NSStringNSNumberNSDictionaryNSArray
Ví dụ: bạn có thể thêm người dùng có setValue như sau:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
Việc sử dụng setValue theo cách này sẽ ghi đè dữ liệu tại vị trí đã chỉ định, bao gồm cả mọi nút con. Tuy nhiên, bạn vẫn có thể cập nhật một phần tử con mà không cần viết lại toàn bộ đối tượng. Nếu muốn cho phép người dùng cập nhật hồ sơ, bạn có thể cập nhật tên người dùng như sau:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Đọc dữ liệu
Đọc dữ liệu bằng cách theo dõi các sự kiện giá trị
Để đọc dữ liệu tại một đường dẫn và theo dõi các thay đổi, hãy sử dụng observeEventType:withBlock của FIRDatabaseReference để quan sát các sự kiện FIRDataEventTypeValue.
| Loại sự kiện | Cách sử dụng thông thường |
|---|---|
FIRDataEventTypeValue |
Đọc và theo dõi các thay đổi đối với toàn bộ nội dung của một đường dẫn. |
Bạn có thể sử dụng sự kiện FIRDataEventTypeValue để đọc dữ liệu tại một đường dẫn nhất định, vì dữ liệu này tồn tại tại thời điểm diễn ra sự kiện. Phương thức này được kích hoạt một lần khi trình nghe được đính kèm và kích hoạt lại mỗi khi dữ liệu (bao gồm cả dữ liệu con) thay đổi. Lệnh gọi lại sự kiện được truyền một snapshot chứa tất cả dữ liệu tại vị trí đó, bao gồm cả dữ liệu con. Nếu không có dữ liệu, ảnh chụp nhanh sẽ trả về false khi bạn gọi exists() và nil khi bạn đọc thuộc tính value của ảnh chụp nhanh.
Ví dụ sau đây minh hoạ một ứng dụng viết blog xã hội truy xuất thông tin chi tiết của một bài đăng từ cơ sở dữ liệu:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
Trình nghe nhận được một FIRDataSnapshot chứa dữ liệu tại vị trí được chỉ định trong cơ sở dữ liệu tại thời điểm xảy ra sự kiện trong thuộc tính value. Bạn có thể gán các giá trị cho loại gốc thích hợp, chẳng hạn như NSDictionary.
Nếu không có dữ liệu nào tại vị trí đó, thì value sẽ là nil.
Đọc dữ liệu một lần
Đọc một lần bằng getData()
SDK này được thiết kế để quản lý các hoạt động tương tác với máy chủ cơ sở dữ liệu cho dù ứng dụng của bạn đang trực tuyến hay ngoại tuyến.
Nhìn chung, bạn nên sử dụng các kỹ thuật sự kiện giá trị được mô tả ở trên để đọc dữ liệu nhằm nhận thông báo về các bản cập nhật cho dữ liệu từ phần phụ trợ. Những kỹ thuật này giúp giảm mức sử dụng và thanh toán, đồng thời được tối ưu hoá để mang đến cho người dùng trải nghiệm tốt nhất khi họ kết nối mạng và không kết nối mạng.
Nếu chỉ cần dữ liệu một lần, bạn có thể sử dụng getData() để lấy thông tin tổng quan nhanh về dữ liệu từ cơ sở dữ liệu. Nếu vì lý do nào đó mà getData() không thể trả về giá trị máy chủ, thì ứng dụng sẽ thăm dò bộ nhớ đệm cục bộ và trả về lỗi nếu vẫn không tìm thấy giá trị.
Ví dụ sau đây minh hoạ cách truy xuất tên người dùng công khai của người dùng một lần từ cơ sở dữ liệu:
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; }];
Việc sử dụng getData() không cần thiết có thể làm tăng mức sử dụng băng thông và dẫn đến giảm hiệu suất. Bạn có thể ngăn chặn điều này bằng cách sử dụng trình nghe theo thời gian thực như minh hoạ ở trên.
Đọc dữ liệu một lần bằng trình quan sát
Trong một số trường hợp, bạn có thể muốn giá trị từ bộ nhớ đệm cục bộ được trả về ngay lập tức, thay vì kiểm tra giá trị đã cập nhật trên máy chủ. Trong những trường hợp đó, bạn có thể sử dụng observeSingleEventOfType để lấy dữ liệu từ bộ nhớ đệm của ổ đĩa cục bộ ngay lập tức.
Điều này hữu ích cho dữ liệu chỉ cần tải một lần và không cần thay đổi thường xuyên hoặc không cần lắng nghe chủ động. Ví dụ: ứng dụng viết blog trong các ví dụ trước sử dụng phương thức này để tải hồ sơ của người dùng khi họ bắt đầu tạo bài đăng mới:
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); }];
Cập nhật hoặc xoá dữ liệu
Cập nhật các trường cụ thể
Để ghi đồng thời vào các phần tử con cụ thể của một nút mà không ghi đè lên các nút con khác, hãy sử dụng phương thức updateChildValues.
Khi gọi updateChildValues, bạn có thể cập nhật các giá trị con ở cấp thấp hơn bằng cách chỉ định đường dẫn cho khoá. Nếu dữ liệu được lưu trữ ở nhiều vị trí để mở rộng quy mô tốt hơn, bạn có thể cập nhật tất cả các phiên bản của dữ liệu đó bằng cách sử dụng tính năng phân phối dữ liệu. Ví dụ: một ứng dụng viết blog xã hội có thể muốn tạo một bài đăng và đồng thời cập nhật bài đăng đó vào bảng tin hoạt động gần đây và bảng tin hoạt động của người dùng đăng bài. Để làm việc này, ứng dụng viết blog sử dụng mã như sau:
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];
Ví dụ này sử dụng childByAutoId để tạo một bài đăng trong nút chứa bài đăng cho tất cả người dùng tại /posts/$postid, đồng thời truy xuất khoá bằng getKey(). Sau đó, bạn có thể dùng khoá này để tạo mục nhập thứ hai trong bài đăng của người dùng tại /user-posts/$userid/$postid.
Khi sử dụng các đường dẫn này, bạn có thể cập nhật đồng thời nhiều vị trí trong cây JSON bằng một lệnh gọi duy nhất đến updateChildValues, chẳng hạn như cách ví dụ này tạo bài đăng mới ở cả hai vị trí. Các bản cập nhật đồng thời được thực hiện theo cách này
có tính nguyên tử: tất cả các bản cập nhật đều thành công hoặc tất cả các bản cập nhật đều không thành công.
Thêm một khối hoàn thành
Nếu muốn biết thời điểm dữ liệu được cam kết, bạn có thể thêm một khối hoàn thành. Cả setValue và updateChildValues đều lấy một khối hoàn thành không bắt buộc được gọi khi quá trình ghi đã được cam kết với cơ sở dữ liệu. Trình nghe này có thể hữu ích trong việc theo dõi dữ liệu nào đã được lưu và dữ liệu nào vẫn đang được đồng bộ hoá. Nếu lệnh gọi không thành công, trình nghe sẽ được truyền một đối tượng lỗi cho biết lý do xảy ra lỗi.
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."); } }];
Xóa dữ liệu
Cách đơn giản nhất để xoá dữ liệu là gọi removeValue trên một tệp đối chiếu đến vị trí của dữ liệu đó.
Bạn cũng có thể xoá bằng cách chỉ định nil làm giá trị cho một thao tác ghi khác, chẳng hạn như setValue hoặc updateChildValues. Bạn có thể sử dụng kỹ thuật này với updateChildValues để xoá nhiều phần tử con trong một lệnh gọi API.
Tách trình nghe
Trình quan sát không tự động ngừng đồng bộ hoá dữ liệu khi bạn rời khỏi một ViewController. Nếu không được xoá đúng cách, trình quan sát sẽ tiếp tục đồng bộ hoá dữ liệu với bộ nhớ cục bộ. Khi không cần trình quan sát nữa, hãy xoá trình quan sát đó bằng cách truyền FIRDatabaseHandle được liên kết đến phương thức removeObserverWithHandle.
Khi bạn thêm một khối lệnh gọi lại vào một tệp đối chiếu, hệ thống sẽ trả về một FIRDatabaseHandle.
Bạn có thể dùng các tên người dùng này để xoá khối lệnh gọi lại.
Nếu nhiều trình nghe đã được thêm vào một tệp đối chiếu cơ sở dữ liệu, thì mỗi trình nghe sẽ được gọi khi một sự kiện được kích hoạt. Để ngừng đồng bộ hoá dữ liệu tại vị trí đó, bạn phải xoá tất cả trình quan sát tại một vị trí bằng cách gọi phương thức removeAllObservers.
Việc gọi removeObserverWithHandle hoặc removeAllObservers trên trình nghe không tự động xoá trình nghe đã đăng ký trên các nút con của trình nghe đó; bạn cũng phải theo dõi các tệp đối chiếu hoặc trình xử lý đó để xoá chúng.
Lưu dữ liệu dưới dạng giao dịch
Khi làm việc với dữ liệu có thể bị hỏng do các nội dung sửa đổi đồng thời, chẳng hạn như bộ đếm gia tăng, bạn có thể sử dụng thao tác giao dịch. Bạn cung cấp cho thao tác này hai đối số: một hàm cập nhật và một lệnh gọi lại hoàn tất không bắt buộc. Hàm cập nhật lấy trạng thái hiện tại của dữ liệu làm đối số và trả về trạng thái mới mà bạn muốn ghi.
Ví dụ: trong ứng dụng viết blog xã hội mẫu, bạn có thể cho phép người dùng gắn dấu sao và bỏ gắn dấu sao cho bài đăng, đồng thời theo dõi số lượng dấu sao mà một bài đăng nhận được như sau:
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); } }];
Việc sử dụng giao dịch giúp ngăn số lượt gắn dấu sao không chính xác nếu nhiều người dùng gắn dấu sao cho cùng một bài đăng cùng lúc hoặc ứng dụng có dữ liệu cũ. Giá trị có trong lớp FIRMutableData ban đầu là giá trị đã biết gần đây nhất của ứng dụng cho đường dẫn hoặc nil nếu không có giá trị nào. Máy chủ so sánh giá trị ban đầu với giá trị hiện tại và chấp nhận giao dịch nếu các giá trị khớp nhau hoặc từ chối giao dịch. Nếu giao dịch bị từ chối, máy chủ sẽ trả về giá trị hiện tại cho ứng dụng, sau đó ứng dụng sẽ chạy lại giao dịch với giá trị đã cập nhật. Quá trình này lặp lại cho đến khi giao dịch được chấp nhận hoặc có quá nhiều lần thử.
Số gia tăng nguyên tử phía máy chủ
Trong trường hợp sử dụng ở trên, chúng ta đang ghi hai giá trị vào cơ sở dữ liệu: mã nhận dạng của người dùng gắn dấu sao/bỏ gắn dấu sao cho bài đăng và số lượng dấu sao tăng lên. Nếu đã biết rằng người dùng đang gắn dấu sao cho bài đăng, chúng ta có thể sử dụng thao tác tăng dần thay vì giao dịch.
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];
Mã này không sử dụng thao tác giao dịch, vì vậy, mã này không tự động chạy lại nếu có bản cập nhật xung đột. Tuy nhiên, vì thao tác tăng dần diễn ra trực tiếp trên máy chủ cơ sở dữ liệu, nên không có khả năng xảy ra xung đột.
Nếu muốn phát hiện và từ chối các xung đột dành riêng cho ứng dụng, chẳng hạn như người dùng gắn dấu sao cho một bài đăng mà họ đã gắn dấu sao trước đó, bạn nên viết các quy tắc bảo mật tuỳ chỉnh cho trường hợp sử dụng đó.
Làm việc với dữ liệu khi không có mạng
Nếu ứng dụng khách mất kết nối mạng, ứng dụng của bạn sẽ tiếp tục hoạt động bình thường.
Mỗi ứng dụng khách kết nối với cơ sở dữ liệu Firebase đều duy trì phiên bản nội bộ riêng của mọi dữ liệu đang hoạt động. Khi dữ liệu được ghi, dữ liệu đó sẽ được ghi vào phiên bản cục bộ này trước tiên. Sau đó, ứng dụng Firebase sẽ đồng bộ hoá dữ liệu đó với máy chủ cơ sở dữ liệu từ xa và với các ứng dụng khác trên cơ sở "nỗ lực tối đa".
Do đó, tất cả các hoạt động ghi vào cơ sở dữ liệu đều kích hoạt các sự kiện cục bộ ngay lập tức, trước khi bất kỳ dữ liệu nào được ghi vào máy chủ. Điều này có nghĩa là ứng dụng của bạn vẫn có thể phản hồi bất kể độ trễ mạng hoặc khả năng kết nối.
Sau khi kết nối được thiết lập lại, ứng dụng của bạn sẽ nhận được một nhóm sự kiện thích hợp để ứng dụng đồng bộ hoá với trạng thái máy chủ hiện tại mà không cần phải viết mã tuỳ chỉnh.
Chúng ta sẽ nói thêm về hành vi ngoại tuyến trong bài viết Tìm hiểu thêm về các chức năng trực tuyến và ngoại tuyến.