Optional: Prototyping und Tests mit Firebase Local Emulator Suite
Bevor wir uns ansehen, wie Ihre App Daten aus Realtime Database liest und in Realtime Database schreibt, stellen wir Ihnen eine Reihe von Tools vor, mit denen Sie Realtime Database-Funktionen prototypisieren und testen können: Firebase Local Emulator Suite. Wenn Sie verschiedene Datenmodelle ausprobieren, Ihre Sicherheitsregeln optimieren oder nach der kostengünstigsten Möglichkeit suchen, mit dem Backend zu interagieren, kann es sinnvoll sein, lokal zu arbeiten, ohne Live-Dienste bereitzustellen.
Ein Realtime Database-Emulator ist Teil der Local Emulator Suite. Dadurch kann Ihre App mit den emulierten Datenbankinhalten und der Konfiguration sowie optional mit den emulierten Projektressourcen (Funktionen, anderen Datenbanken und Sicherheitsregeln) interagieren.
Die Verwendung des Realtime Database-Emulators umfasst nur wenige Schritte:
- Fügen Sie der Testkonfiguration Ihrer App eine Codezeile hinzu, um eine Verbindung zum Emulator herzustellen.
- Führen Sie
firebase emulators:start
im Stammverzeichnis Ihres lokalen Projektverzeichnisses aus. - Aufrufe aus dem Prototypcode Ihrer App mit einem Realtime Database-Plattform-SDK wie gewohnt oder mit der Realtime Database-REST-API ausführen.
Eine detaillierte Schritt-für-Schritt-Anleitung für Realtime Database und Cloud Functions ist verfügbar. Sehen Sie sich auch die Einführung in Local Emulator Suite an.
FIRDatabaseReference abrufen
Wenn Sie Daten aus der Datenbank lesen oder in die Datenbank schreiben möchten, benötigen Sie eine Instanz von FIRDatabaseReference
:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Daten schreiben
In diesem Dokument werden die Grundlagen zum Lesen und Schreiben von Firebase-Daten behandelt.
Firebase-Daten werden in eine Database
-Referenz geschrieben und abgerufen, indem ein asynchroner Listener an die Referenz angehängt wird. Der Listener wird einmal für den anfänglichen Status der Daten und dann bei jeder Änderung der Daten ausgelöst.
Grundlegende Schreibvorgänge
Für einfache Schreibvorgänge können Sie setValue
verwenden, um Daten an einem angegebenen Verweis zu speichern und alle vorhandenen Daten an diesem Pfad zu ersetzen. Mit dieser Methode können Sie Folgendes tun:
- Übergeben Sie die Pass-Typen, die den verfügbaren JSON-Typen entsprechen, so:
NSString
NSNumber
NSDictionary
NSArray
So können Sie beispielsweise einen Nutzer mit setValue
hinzufügen:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
Wenn Sie setValue
auf diese Weise verwenden, werden Daten am angegebenen Speicherort überschrieben, einschließlich aller untergeordneten Knoten. Sie können ein untergeordnetes Element jedoch weiterhin aktualisieren, ohne das gesamte Objekt neu zu schreiben. Wenn Sie Nutzern erlauben möchten, ihre Profile zu aktualisieren, können Sie den Nutzernamen so aktualisieren:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Daten lesen
Daten lesen, indem Sie auf Wertänderungsereignisse warten
Wenn Sie Daten an einem Pfad lesen und auf Änderungen warten möchten, verwenden Sie die observeEventType:withBlock
von FIRDatabaseReference
, um FIRDataEventTypeValue
-Ereignisse zu beobachten.
Ereignistyp | Typische Verwendung |
---|---|
FIRDataEventTypeValue |
Änderungen am gesamten Inhalt eines Pfads lesen und überwachen. |
Mit dem FIRDataEventTypeValue
-Ereignis können Sie die Daten an einem bestimmten Pfad lesen, wie sie zum Zeitpunkt des Ereignisses vorhanden sind. Diese Methode wird einmal ausgelöst, wenn der Listener angehängt wird, und dann jedes Mal, wenn sich die Daten ändern, einschließlich aller untergeordneten Elemente. An den Ereignis-Callback wird ein snapshot
übergeben, das alle Daten an diesem Speicherort, einschließlich untergeordneter Daten, enthält. Wenn keine Daten vorhanden sind, wird für den Snapshot false
zurückgegeben, wenn Sie exists()
aufrufen, und nil
, wenn Sie die value
-Eigenschaft lesen.
Das folgende Beispiel zeigt eine Social-Blogging-Anwendung, die die Details eines Beitrags aus der Datenbank abruft:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
Der Listener empfängt ein FIRDataSnapshot
, das die Daten am angegebenen Speicherort 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 am Standort keine Daten vorhanden sind, ist value
gleich nil
.
Daten einmal lesen
Einmaliges 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 Techniken für Wert-Ereignisse verwenden, um Daten zu lesen und über Aktualisierungen der Daten aus dem Backend benachrichtigt zu werden. Diese Techniken reduzieren die Nutzung und Abrechnung und sind so optimiert, dass Ihre Nutzer beim Online- und Offlinegehen die bestmögliche Erfahrung haben.
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, fragt der Client den lokalen Speicher-Cache ab und gibt einen Fehler zurück, wenn der Wert immer noch nicht gefunden wird.
Das folgende Beispiel zeigt, wie der öffentliche Nutzername eines Nutzers einmal aus der Datenbank abgerufen wird:
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; }];
Die unnötige Verwendung von getData()
kann die Bandbreitennutzung erhöhen und zu Leistungseinbußen führen. Das lässt sich durch die Verwendung eines Echtzeit-Listeners wie oben gezeigt vermeiden.
Daten einmal mit einem Observer lesen
In einigen Fällen soll der Wert aus dem lokalen Cache sofort zurückgegeben werden, 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.
Das 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. Die Blogging-App in den vorherigen Beispielen verwendet diese Methode beispielsweise, um das Profil eines Nutzers zu laden, wenn er einen neuen Beitrag verfasst:
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); }];
Daten aktualisieren oder löschen
Bestimmte Felder aktualisieren
Wenn Sie gleichzeitig in bestimmte untergeordnete Knoten eines Knotens schreiben möchten, ohne andere untergeordnete Knoten zu überschreiben, verwenden Sie die Methode updateChildValues
.
Wenn Sie updateChildValues
aufrufen, 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 Standorten gespeichert werden, können Sie alle Instanzen dieser Daten mit Data Fan-Out aktualisieren. Eine Social-Blogging-App möchte beispielsweise einen Beitrag erstellen und ihn gleichzeitig im Feed für die letzten Aktivitäten und im Aktivitätsfeed des Nutzers, der den Beitrag erstellt hat, aktualisieren. Dazu verwendet die Blogging-Anwendung Code wie diesen:
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];
In diesem Beispiel wird childByAutoId
verwendet, um einen Beitrag im Knoten mit Beiträgen für alle Nutzer unter /posts/$postid
zu erstellen und gleichzeitig den Schlüssel mit getKey()
abzurufen. Der Schlüssel kann dann verwendet werden, um einen zweiten Eintrag in den Beiträgen des Nutzers unter /user-posts/$userid/$postid
zu erstellen.
Mit diesen Pfaden können Sie mit einem einzigen Aufruf von updateChildValues
mehrere Orte im JSON-Baum gleichzeitig aktualisieren. Im folgenden Beispiel wird der neue Beitrag an beiden Orten erstellt. Gleichzeitige Aktualisierungen auf diese Weise sind atomar: Entweder sind alle Aktualisierungen erfolgreich oder alle Aktualisierungen schlagen fehl.
Abschlussblock hinzufügen
Wenn Sie wissen möchten, wann Ihre Daten übertragen wurden, können Sie einen Abschlussblock hinzufügen. Sowohl setValue
als auch updateChildValues
akzeptieren einen optionalen Abschlussblock, der aufgerufen wird, wenn der Schreibvorgang in der Datenbank übernommen wurde. Dieser Listener kann hilfreich 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.
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."); } }];
Daten löschen
Am einfachsten löschen Sie Daten, indem Sie removeValue
für eine Referenz auf den Speicherort dieser Daten aufrufen.
Sie können auch löschen, indem Sie nil
als Wert für einen anderen Schreibvorgang wie setValue
oder updateChildValues
angeben. Sie können diese Technik mit updateChildValues
verwenden, um mehrere untergeordnete Elemente in einem einzigen API-Aufruf zu löschen.
Listener trennen
Die Synchronisierung von Daten wird nicht automatisch beendet, wenn Sie eine ViewController
verlassen. Wenn ein Observer nicht ordnungsgemäß entfernt wird, werden weiterhin Daten mit dem lokalen Speicher synchronisiert. Wenn ein Observer nicht mehr benötigt wird, entfernen Sie ihn, indem Sie das zugehörige FIRDatabaseHandle
an die Methode removeObserverWithHandle
übergeben.
Wenn Sie einer Referenz einen Callback-Block hinzufügen, wird FIRDatabaseHandle
zurückgegeben.
Mit diesen Handles kann der Callback-Block entfernt werden.
Wenn einer Datenbankreferenz mehrere Listener hinzugefügt wurden, wird jeder Listener aufgerufen, wenn ein Ereignis ausgelöst wird. Wenn Sie die Synchronisierung von Daten an diesem Speicherort beenden möchten, müssen Sie alle Beobachter an einem Speicherort entfernen. Rufen Sie dazu die Methode removeAllObservers
auf.
Durch Aufrufen von removeObserverWithHandle
oder removeAllObservers
für einen Listener werden nicht automatisch Listener entfernt, die für seine untergeordneten Knoten registriert sind. Sie müssen auch diese Referenzen oder Handles im Blick behalten, um sie zu entfernen.
Daten als Transaktionen speichern
Wenn Sie mit Daten arbeiten, die durch gleichzeitige Änderungen beschädigt werden könnten, z. B. inkrementelle Zähler, können Sie einen Transaktionsvorgang verwenden. Für diesen Vorgang werden zwei Argumente angegeben: eine Aktualisierungsfunktion und ein optionaler Abschluss-Callback. Die Aktualisierungsfunktion verwendet den aktuellen Status der Daten als Argument und gibt den neuen gewünschten Status zurück, den Sie schreiben möchten.
In der Beispiel-App für Social Blogging könnten Sie Nutzern beispielsweise erlauben, Beiträge mit Sternen zu markieren und die Anzahl der Sterne für einen Beitrag so zu verfolgen:
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); } }];
Durch die Verwendung einer Transaktion wird verhindert, dass die Anzahl der Sterne falsch ist, wenn mehrere Nutzer denselben Beitrag gleichzeitig mit einem Stern markieren oder der Client veraltete Daten hatte. Der Wert in der Klasse FIRMutableData
ist anfangs der letzte bekannte Wert des Clients für den Pfad oder nil
, falls kein Wert vorhanden ist. Der Server vergleicht den ursprünglichen Wert mit dem 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 noch einmal ausführt. Dieser Vorgang wird wiederholt, bis die Transaktion akzeptiert wird oder zu viele Versuche unternommen wurden.
Atomare serverseitige Steigerungen
Im obigen Anwendungsfall schreiben wir zwei Werte in die Datenbank: die ID des Nutzers, der den Beitrag mit einem Stern markiert oder die Markierung entfernt, und die erhöhte Anzahl der Sterne. Wenn wir bereits wissen, dass der Nutzer den Beitrag mit einem Sternchen markiert, können wir anstelle einer Transaktion einen atomaren Inkrementvorgang verwenden.
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];
In diesem Code wird kein Transaktionsvorgang verwendet. Er wird also nicht automatisch noch einmal ausgeführt, wenn es zu einem Konflikt kommt. Da der Inkrementierungsvorgang jedoch direkt auf dem Datenbankserver erfolgt, besteht keine Konfliktgefahr.
Wenn Sie anwendungsspezifische Konflikte erkennen und ablehnen möchten, z. B. wenn ein Nutzer einen Beitrag mit einem Stern markiert, den er bereits zuvor mit einem Stern markiert hat, sollten Sie benutzerdefinierte Sicherheitsregeln für diesen Anwendungsfall schreiben.
Offline mit Daten arbeiten
Wenn ein Client die 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 auf Best-Effort-Basis mit den Remote-Datenbankservern und 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 der Netzwerklatenz oder der Verbindung reaktionsfähig bleibt.
Sobald die Verbindung wiederhergestellt ist, erhält Ihre App die entsprechenden Ereignisse, sodass der Client mit dem aktuellen Serverstatus synchronisiert wird, ohne dass Sie benutzerdefinierten Code schreiben müssen.
Weitere Informationen zum Offlineverhalten finden Sie unter Online- und Offlinefunktionen.