(Optional) Erstellen Sie einen Prototyp und testen Sie ihn mit der Firebase Local Emulator Suite
Bevor wir darüber sprechen, wie Ihre App aus der Realtime Database liest und in sie schreibt, stellen wir Ihnen eine Reihe von Tools vor, die Sie zum Prototyping und Testen der Realtime Database-Funktionalität verwenden können: Firebase Local Emulator Suite. Wenn Sie verschiedene Datenmodelle ausprobieren, Ihre Sicherheitsregeln optimieren oder daran arbeiten, den kostengünstigsten Weg zur Interaktion mit dem Back-End zu finden, kann es eine gute Idee sein, lokal arbeiten zu können, ohne Live-Dienste bereitzustellen.
Ein Realtime Database-Emulator ist Teil der Local Emulator Suite, die es Ihrer App ermöglicht, mit Ihren emulierten Datenbankinhalten und -konfigurationen sowie optional mit Ihren emulierten Projektressourcen (Funktionen, andere Datenbanken und Sicherheitsregeln) zu interagieren.
Die Verwendung des Realtime Database-Emulators umfasst nur wenige Schritte:
- Hinzufügen einer Codezeile zur Testkonfiguration Ihrer App, um eine Verbindung mit dem Emulator herzustellen.
- Führen Sie im Stammverzeichnis Ihres lokalen Projektverzeichnisses
firebase emulators:start
aus. - Aufrufe aus dem Prototypcode Ihrer App wie gewohnt mit einem SDK der Realtime Database-Plattform oder mit der REST-API der Realtime Database.
Eine detaillierte Anleitung zu Realtime Database und Cloud Functions ist verfügbar. Sie sollten sich auch die Einführung in die Local Emulator Suite ansehen .
Holen Sie sich eine FIRDatabaseReference
Um Daten aus der Datenbank zu lesen oder zu schreiben, benötigen Sie eine Instanz von FIRDatabaseReference
:
Schnell
var ref: DatabaseReference! ref = Database.database().reference()
Ziel c
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Daten schreiben
Dieses Dokument behandelt die Grundlagen zum Lesen und Schreiben von Firebase-Daten.
Firebase-Daten werden in eine Database
geschrieben und abgerufen, indem ein asynchroner Listener an die Referenz angehängt wird. Der Listener wird einmal für den Anfangszustand der Daten ausgelöst und erneut, wenn sich die Daten ändern.
Grundlegende Schreiboperationen
Für grundlegende Schreibvorgänge können Sie setValue
verwenden, um Daten in einer angegebenen Referenz zu speichern und alle vorhandenen Daten in diesem Pfad zu ersetzen. Sie können diese Methode verwenden, um:
- Übergeben Sie Typen, die den verfügbaren JSON-Typen entsprechen, wie folgt:
-
NSString
-
NSNumber
-
NSDictionary
-
NSArray
-
Beispielsweise können Sie einen Benutzer mit setValue
wie folgt hinzufügen:
Schnell
self.ref.child("users").child(user.uid).setValue(["username": username])
Ziel c
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
Die Verwendung setValue
auf diese Weise überschreibt Daten an der angegebenen Position, einschließlich aller untergeordneten Knoten. Sie können jedoch ein untergeordnetes Objekt aktualisieren, ohne das gesamte Objekt neu zu schreiben. Wenn Sie Benutzern erlauben möchten, ihre Profile zu aktualisieren, können Sie den Benutzernamen wie folgt aktualisieren:
Schnell
self.ref.child("users/\(user.uid)/username").setValue(username)
Ziel c
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Daten lesen
Lesen Sie Daten, indem Sie auf Wertereignisse lauschen
Um Daten in einem Pfad zu lesen und auf Änderungen zu lauschen, verwenden Sie den observeEventType:withBlock
von FIRDatabaseReference
, um FIRDataEventTypeValue
Ereignisse zu beobachten.
Ereignistyp | Typische Verwendung |
---|---|
FIRDataEventTypeValue | Lesen und hören Sie auf Änderungen am gesamten Inhalt eines Pfads. |
Sie können das Ereignis FIRDataEventTypeValue
verwenden, um die Daten in einem bestimmten Pfad so zu lesen, wie sie zum Zeitpunkt des Ereignisses vorhanden sind. Diese Methode wird einmal ausgelöst, wenn der Listener angehängt wird, und jedes Mal, wenn sich die Daten, einschließlich aller untergeordneten Elemente, ändern. Dem Ereignis-Callback wird ein snapshot
übergeben, der alle Daten an diesem Speicherort enthält, einschließlich untergeordneter Daten. Wenn keine Daten vorhanden sind, gibt der Snapshot false
zurück, wenn Sie exists()
aufrufen, und nil
, wenn Sie seine value
-Eigenschaft lesen.
Das folgende Beispiel zeigt eine Social-Blogging-Anwendung, die die Details eines Beitrags aus der Datenbank abruft:
Schnell
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Ziel c
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
Der Listener erhält einen FIRDataSnapshot
, der die Daten an der angegebenen Stelle in der Datenbank zum Zeitpunkt des Ereignisses in seiner value
-Eigenschaft enthält. Sie können die Werte dem entsprechenden nativen Typ zuweisen, z. B. NSDictionary
. Wenn an der Position keine Daten vorhanden sind, ist der value
nil
.
Daten einmal lesen
Einmal lesen mit getData()
Das SDK wurde entwickelt, um Interaktionen mit Datenbankservern zu verwalten, unabhängig davon, ob Ihre App online oder offline ist.
Im Allgemeinen sollten Sie die oben beschriebenen Wertereignistechniken verwenden, um Daten zu lesen, um vom Back-End über Aktualisierungen der Daten benachrichtigt zu werden. Diese Techniken reduzieren Ihre Nutzung und Abrechnung und sind optimiert, um Ihren Benutzern die beste Erfahrung zu bieten, wenn sie online und offline gehen.
Wenn Sie die Daten nur einmal benötigen, können Sie mit getData()
einen Snapshot der Daten aus der Datenbank abrufen. Wenn getData()
aus irgendeinem Grund den Serverwert nicht zurückgeben kann, prüft der Client den lokalen Speichercache und gibt einen Fehler zurück, wenn der Wert immer noch nicht gefunden wird.
Das folgende Beispiel zeigt das einmalige Abrufen des öffentlich zugänglichen Benutzernamens eines Benutzers aus der Datenbank:
Schnell
ref.child("users/\(uid)/username").getData(completion: { error, snapshot in guard error == nil else { print(error!.localizedDescription) return; } let userName = snapshot.value as? String ?? "Unknown"; });
Ziel 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; }];
Die unnötige Verwendung von getData()
kann die Bandbreitennutzung erhöhen und zu Leistungsverlusten führen, was durch die Verwendung eines Echtzeit-Listeners wie oben gezeigt verhindert werden kann.
Lesen Sie die Daten einmal mit einem Beobachter ab
In einigen Fällen möchten Sie möglicherweise, dass der Wert aus dem lokalen Cache sofort zurückgegeben wird, anstatt auf dem Server nach einem aktualisierten Wert zu suchen. In diesen Fällen können Sie observeSingleEventOfType
verwenden, um die Daten sofort aus dem lokalen Festplatten-Cache abzurufen.
Dies ist nützlich für Daten, die nur einmal geladen werden müssen und sich voraussichtlich nicht häufig ändern oder aktives Zuhören erfordern. Beispielsweise verwendet die Blogging-App in den vorherigen Beispielen diese Methode, um das Profil eines Benutzers zu laden, wenn er mit dem Verfassen eines neuen Beitrags beginnt:
Schnell
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) }
Ziel 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); }];
Aktualisieren oder Löschen von Daten
Aktualisieren Sie bestimmte Felder
Um gleichzeitig in bestimmte untergeordnete Knoten eines Knotens zu schreiben, ohne andere untergeordnete Knoten zu überschreiben, verwenden Sie die updateChildValues
Methode.
Beim Aufrufen updateChildValues
können Sie untergeordnete Werte auf niedrigerer Ebene aktualisieren, indem Sie einen Pfad für den Schlüssel angeben. Wenn Daten zur besseren Skalierung an mehreren Speicherorten gespeichert werden, können Sie alle Instanzen dieser Daten mithilfe von Datenauffächerung aktualisieren. Beispielsweise möchte eine Social-Blogging-App möglicherweise einen Beitrag erstellen und ihn gleichzeitig mit dem Feed für die letzten Aktivitäten und dem Aktivitätsfeed des postenden Benutzers aktualisieren. Dazu verwendet die Blogging-Anwendung Code wie diesen:
Schnell
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)
Ziel 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];
In diesem Beispiel wird childByAutoId
verwendet, um einen Beitrag im Knoten zu erstellen, der Beiträge für alle Benutzer unter /posts/$postid
, und gleichzeitig den Schlüssel mit getKey()
. Der Schlüssel kann dann verwendet werden, um einen zweiten Eintrag in den Beiträgen des Benutzers unter /user-posts/$userid/$postid
zu erstellen.
Mithilfe dieser Pfade können Sie mit einem einzigen Aufruf von updateChildValues
simultane Aktualisierungen an mehreren Stellen in der JSON-Struktur durchführen, z. B. wie in diesem Beispiel der neue Beitrag an beiden Stellen erstellt wird. Auf diese Weise durchgeführte gleichzeitige Aktualisierungen sind atomar: Entweder sind alle Aktualisierungen erfolgreich oder alle Aktualisierungen schlagen fehl.
Füge einen Abschlussblock hinzu
Wenn Sie wissen möchten, wann Ihre Daten übertragen wurden, können Sie einen Vervollständigungsblock hinzufügen. Sowohl setValue
als auch updateChildValues
einen optionalen Abschlussblock, der aufgerufen wird, wenn der Schreibvorgang an die Datenbank übertragen wurde. Dieser Listener kann nützlich sein, um zu verfolgen, welche Daten gespeichert wurden und welche Daten noch synchronisiert werden. Wenn der Aufruf nicht erfolgreich war, wird dem Listener ein Fehlerobjekt übergeben, das angibt, warum der Fehler aufgetreten ist.
Schnell
ref.child("users").child(user.uid).setValue(["username": username]) { (error:Error?, ref:DatabaseReference) in if let error = error { print("Data could not be saved: \(error).") } else { print("Data saved successfully!") } }
Ziel 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."); } }];
Daten löschen
Die einfachste Methode zum Löschen von Daten besteht darin, removeValue
für einen Verweis auf den Speicherort dieser Daten aufzurufen.
Sie können auch löschen, indem Sie nil
als Wert für einen anderen Schreibvorgang wie setValue
oder updateChildValues
. Sie können diese Technik mit updateChildValues
, um mehrere untergeordnete Elemente in einem einzigen API-Aufruf zu löschen.
Zuhörer trennen
Beobachter hören nicht automatisch auf, Daten zu synchronisieren, wenn Sie einen ViewController
verlassen. Wenn ein Beobachter nicht ordnungsgemäß entfernt wird, synchronisiert er weiterhin Daten mit dem lokalen Speicher. Wenn ein Beobachter nicht mehr benötigt wird, entfernen Sie ihn, indem Sie das zugeordnete FIRDatabaseHandle
an die Methode removeObserverWithHandle
.
Wenn Sie einem Verweis einen Rückrufblock hinzufügen, wird ein FIRDatabaseHandle
zurückgegeben. Diese Handles können verwendet werden, um die Callback-Sperre zu entfernen.
Wenn einer Datenbankreferenz mehrere Listener hinzugefügt wurden, wird jeder Listener aufgerufen, wenn ein Ereignis ausgelöst wird. Um die Synchronisierung von Daten an diesem Standort zu beenden, müssen Sie alle Beobachter an einem Standort entfernen, indem Sie die Methode removeAllObservers
aufrufen.
Das Aufrufen removeObserverWithHandle
oder removeAllObservers
auf einem Listener entfernt nicht automatisch Listener, die auf seinen untergeordneten Knoten registriert sind; Sie müssen diese Verweise oder Handles auch verfolgen, um sie zu entfernen.
Speichern Sie Daten als Transaktionen
Wenn Sie mit Daten arbeiten, die durch gleichzeitige Änderungen beschädigt werden könnten, wie z. B. inkrementelle Zähler, können Sie eine Transaktionsoperation verwenden . Sie geben dieser Operation zwei Argumente: eine Update-Funktion und einen optionalen Abschluss-Callback. Die Update-Funktion nimmt den aktuellen Zustand der Daten als Argument und gibt den neuen gewünschten Zustand zurück, den Sie schreiben möchten.
In der Beispiel-Social-Blogging-App könnten Sie beispielsweise Benutzern erlauben, Beiträge zu markieren und die Markierung aufzuheben, und nachverfolgen, wie viele Sterne ein Beitrag erhalten hat, wie folgt:
Schnell
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) } }
Ziel 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); } }];
Durch die Verwendung einer Transaktion wird verhindert, dass die Anzahl der Sterne falsch ist, wenn mehrere Benutzer denselben Beitrag gleichzeitig markieren oder der Client veraltete Daten hatte. Der in der Klasse FIRMutableData
enthaltene Wert ist anfänglich der letzte bekannte Wert des Clients für den Pfad oder nil
, wenn es keinen gibt. Der Server vergleicht den Anfangswert mit seinem aktuellen Wert und akzeptiert die Transaktion, wenn die Werte übereinstimmen, oder lehnt sie ab. Wenn die Transaktion abgelehnt wird, gibt der Server den aktuellen Wert an den Client zurück, der die Transaktion mit dem aktualisierten Wert erneut ausführt. Dies wiederholt sich, bis die Transaktion akzeptiert wird oder zu viele Versuche unternommen wurden.
Atomare serverseitige Inkremente
Im obigen Anwendungsfall schreiben wir zwei Werte in die Datenbank: die ID des Benutzers, der den Beitrag markiert/entmarkiert, und die erhöhte Anzahl der Sterne. Wenn wir bereits wissen, dass der Benutzer den Beitrag markiert, können wir anstelle einer Transaktion eine atomare Inkrementoperation verwenden.
Schnell
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);
Ziel 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];
Dieser Code verwendet keine Transaktionsoperation, sodass er nicht automatisch erneut ausgeführt wird, wenn eine widersprüchliche Aktualisierung vorliegt. Da die Inkrementierungsoperation jedoch direkt auf dem Datenbankserver stattfindet, besteht keine Möglichkeit eines Konflikts.
Wenn Sie anwendungsspezifische Konflikte erkennen und zurückweisen möchten, z. B. wenn ein Benutzer einen Beitrag markiert, den er bereits zuvor markiert hat, sollten Sie benutzerdefinierte Sicherheitsregeln für diesen Anwendungsfall schreiben.
Offline mit Daten arbeiten
Wenn ein Client seine Netzwerkverbindung verliert, funktioniert Ihre App weiterhin ordnungsgemäß.
Jeder Client, der mit einer Firebase-Datenbank verbunden ist, verwaltet seine eigene interne Version aller aktiven Daten. Wenn Daten geschrieben werden, werden sie zuerst in diese lokale Version geschrieben. Der Firebase-Client synchronisiert diese Daten dann nach bestem Wissen und Gewissen mit den Remote-Datenbankservern und mit anderen Clients.
Daher lösen alle Schreibvorgänge in die Datenbank sofort lokale Ereignisse aus, bevor Daten auf den Server geschrieben werden. Das bedeutet, dass Ihre App unabhängig von Netzwerklatenz oder Konnektivität reaktionsfähig bleibt.
Sobald die Konnektivität wiederhergestellt ist, empfängt Ihre App die entsprechenden Ereignisse, sodass der Client mit dem aktuellen Serverstatus synchronisiert wird, ohne benutzerdefinierten Code schreiben zu müssen.
Wir werden mehr über das Offline-Verhalten in Erfahren Sie mehr über Online- und Offline-Funktionen sprechen.