(Facultatif) Prototyper et tester avec Firebase Local Emulator Suite
Avant de parler de la façon dont votre application lit et écrit dans Realtime Database, présentons un ensemble d'outils que vous pouvez utiliser pour prototyper et tester la fonctionnalité Realtime Database : Firebase Local Emulator Suite. Si vous essayez différents modèles de données, optimisez vos règles de sécurité ou cherchez le moyen le plus économique d'interagir avec le backend, il peut être judicieux de pouvoir travailler en local sans déployer de services en direct.
Un émulateur Realtime Database fait partie de Local Emulator Suite, qui permet à votre application d'interagir avec le contenu et la configuration de votre base de données émulée, ainsi qu'éventuellement avec les ressources de votre projet émulé (fonctions, autres bases de données et règles de sécurité).
L'utilisation de l'émulateur Realtime Database ne nécessite que quelques étapes :
- Ajoutez une ligne de code à la configuration de test de votre application pour vous connecter à l'émulateur.
- À partir de la racine du répertoire de votre projet local, exécutez
firebase emulators:start
. - Effectuez des appels à partir du code prototype de votre application à l'aide d'un SDK de plate-forme Realtime Database comme d'habitude, ou à l'aide de l'API REST Realtime Database.
Un tutoriel détaillé sur Realtime Database et Cloud Functions est disponible. Nous vous conseillons également de consulter l'introduction à Local Emulator Suite.
Obtenir une FIRDatabaseReference
Pour lire ou écrire des données dans la base de données, vous avez besoin d'une instance de FIRDatabaseReference
:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Écrire des données
Ce document décrit les principes de base de la lecture et de l'écriture de données Firebase.
Les données Firebase sont écrites dans une référence Database
et récupérées en attachant un écouteur asynchrone à la référence. L'écouteur est déclenché une fois pour l'état initial des données, puis chaque fois que les données changent.
Opérations d'écriture de base
Pour les opérations d'écriture de base, vous pouvez utiliser setValue
pour enregistrer des données dans une référence spécifiée, en remplaçant toutes les données existantes à ce chemin d'accès. Vous pouvez utiliser cette méthode pour :
- Faites correspondre les types de pass aux types JSON disponibles comme suit :
NSString
NSNumber
NSDictionary
NSArray
Par exemple, vous pouvez ajouter un utilisateur avec setValue
comme suit :
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
L'utilisation de setValue
de cette manière écrase les données à l'emplacement spécifié, y compris tous les nœuds enfants. Toutefois, vous pouvez toujours mettre à jour un enfant sans réécrire l'intégralité de l'objet. Si vous souhaitez autoriser les utilisateurs à modifier leur profil, vous pouvez mettre à jour le nom d'utilisateur comme suit :
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Lire des données
Lire les données en écoutant les événements de valeur
Pour lire des données à un chemin et écouter les modifications, utilisez observeEventType:withBlock
de FIRDatabaseReference
pour observer les événements FIRDataEventTypeValue
.
Type d'événement | Utilisation typique |
---|---|
FIRDataEventTypeValue |
Lire et écouter les modifications apportées à l'ensemble du contenu d'un chemin. |
Vous pouvez utiliser l'événement FIRDataEventTypeValue
pour lire les données à un chemin d'accès donné, telles qu'elles existent au moment de l'événement. Cette méthode est déclenchée une fois lorsque l'écouteur est associé, puis chaque fois que les données, y compris les enfants, changent. Un snapshot
contenant toutes les données à cet emplacement, y compris les données enfants, est transmis au rappel d'événement. S'il n'y a pas de données, l'instantané renverra false
lorsque vous appellerez exists()
et nil
lorsque vous lirez sa propriété value
.
L'exemple suivant montre une application de blog social qui récupère les détails d'un post à partir de la base de données :
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
L'écouteur reçoit un FIRDataSnapshot
qui contient les données à l'emplacement spécifié dans la base de données au moment de l'événement dans sa propriété value
. Vous pouvez attribuer les valeurs au type natif approprié, tel que NSDictionary
.
Si aucune donnée n'existe à l'emplacement, value
est nil
.
Lire les données une seule fois
Lecture unique à l'aide de getData()
Le SDK est conçu pour gérer les interactions avec les serveurs de base de données, que votre application soit en ligne ou hors connexion.
En règle générale, vous devez utiliser les techniques d'événements de valeur décrites ci-dessus pour lire les données et être averti des mises à jour des données provenant du backend. Ces techniques réduisent votre utilisation et votre facturation, et sont optimisées pour offrir à vos utilisateurs la meilleure expérience lorsqu'ils se connectent et se déconnectent.
Si vous n'avez besoin des données qu'une seule fois, vous pouvez utiliser getData()
pour obtenir un instantané des données de la base de données. Si, pour une raison quelconque, getData()
ne parvient pas à renvoyer la valeur du serveur, le client sonde le cache de stockage local et renvoie une erreur si la valeur est toujours introuvable.
L'exemple suivant montre comment récupérer le nom d'utilisateur public d'un utilisateur une seule fois à partir de la base de données :
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; }];
L'utilisation inutile de getData()
peut augmenter l'utilisation de la bande passante et entraîner une perte de performances, ce qui peut être évité en utilisant un écouteur en temps réel comme indiqué ci-dessus.
Lire les données une seule fois avec un observateur
Dans certains cas, vous pouvez souhaiter que la valeur du cache local soit renvoyée immédiatement, au lieu de vérifier si une valeur mise à jour est disponible sur le serveur. Dans ce cas, vous pouvez utiliser observeSingleEventOfType
pour obtenir immédiatement les données du cache de disque local.
Cela est utile pour les données qui ne doivent être chargées qu'une seule fois et qui ne sont pas censées changer fréquemment ni nécessiter une écoute active. Par exemple, l'application de blog des exemples précédents utilise cette méthode pour charger le profil d'un utilisateur lorsqu'il commence à rédiger un nouvel article :
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); }];
Mettre à jour ou supprimer des données
Mettre à jour des champs spécifiques
Pour écrire simultanément dans des enfants spécifiques d'un nœud sans écraser d'autres nœuds enfants, utilisez la méthode updateChildValues
.
Lorsque vous appelez updateChildValues
, vous pouvez mettre à jour les valeurs enfants de niveau inférieur en spécifiant un chemin d'accès pour la clé. Si les données sont stockées à plusieurs endroits pour mieux évoluer, vous pouvez mettre à jour toutes les instances de ces données à l'aide de la distribution des données. Par exemple, une application de blog social peut vouloir créer un post et le mettre à jour simultanément dans le flux d'activité récente et dans le flux d'activité de l'utilisateur qui a publié le post. Pour ce faire, l'application de blog utilise un code comme celui-ci :
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];
Cet exemple utilise childByAutoId
pour créer un post dans le nœud contenant les posts de tous les utilisateurs à l'adresse /posts/$postid
et récupérer simultanément la clé avec getKey()
. La clé peut ensuite être utilisée pour créer une deuxième entrée dans les posts de l'utilisateur à l'adresse /user-posts/$userid/$postid
.
Ces chemins vous permettent de mettre à jour simultanément plusieurs emplacements dans l'arborescence JSON avec un seul appel à updateChildValues
, comme le montre cet exemple qui crée le nouveau post aux deux emplacements. Les mises à jour simultanées effectuées de cette manière sont atomiques : soit toutes les mises à jour réussissent, soit toutes échouent.
Ajouter un bloc d'achèvement
Si vous souhaitez savoir quand vos données ont été validées, vous pouvez ajouter un bloc d'achèvement. setValue
et updateChildValues
acceptent un bloc d'achèvement facultatif qui est appelé lorsque l'écriture a été validée dans la base de données. Ce listener peut être utile pour suivre les données qui ont été enregistrées et celles qui sont encore en cours de synchronisation. Si l'appel a échoué, un objet d'erreur est transmis au listener pour indiquer la raison de l'échec.
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."); } }];
Supprimer des données
Le moyen le plus simple de supprimer des données consiste à appeler removeValue
sur une référence à l'emplacement de ces données.
Vous pouvez également supprimer une valeur en spécifiant nil
comme valeur pour une autre opération d'écriture, telle que setValue
ou updateChildValues
. Vous pouvez utiliser cette technique avec updateChildValues
pour supprimer plusieurs enfants en un seul appel d'API.
Détacher les écouteurs
Les observateurs ne cessent pas automatiquement de synchroniser les données lorsque vous quittez un ViewController
. Si un observateur n'est pas supprimé correctement, il continue de synchroniser les données avec la mémoire locale. Lorsqu'un observateur n'est plus nécessaire, supprimez-le en transmettant le FIRDatabaseHandle
associé à la méthode removeObserverWithHandle
.
Lorsque vous ajoutez un bloc de rappel à une référence, un FIRDatabaseHandle
est renvoyé.
Ces handles peuvent être utilisés pour supprimer le bloc de rappel.
Si plusieurs écouteurs ont été ajoutés à une référence de base de données, chacun d'eux est appelé lorsqu'un événement est déclenché. Pour arrêter la synchronisation des données à cet emplacement, vous devez supprimer tous les observateurs à un emplacement en appelant la méthode removeAllObservers
.
L'appel de removeObserverWithHandle
ou removeAllObservers
sur un écouteur ne supprime pas automatiquement les écouteurs enregistrés sur ses nœuds enfants. Vous devez également garder une trace de ces références ou de ces handles pour les supprimer.
Enregistrer des données en tant que transactions
Lorsque vous travaillez avec des données qui pourraient être corrompues par des modifications simultanées, comme des compteurs incrémentaux, vous pouvez utiliser une opération de transaction. Vous fournissez deux arguments à cette opération : une fonction de mise à jour et un rappel d'achèvement facultatif. La fonction de mise à jour prend l'état actuel des données comme argument et renvoie le nouvel état souhaité que vous souhaitez écrire.
Par exemple, dans l'application de blog social, vous pouvez autoriser les utilisateurs à ajouter des posts à leurs favoris et à les en retirer, et suivre le nombre de favoris qu'un post a reçus comme suit :
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); } }];
L'utilisation d'une transaction permet d'éviter que le nombre d'étoiles soit incorrect si plusieurs utilisateurs ajoutent la même publication à leurs favoris en même temps ou si le client disposait de données obsolètes. La valeur contenue dans la classe FIRMutableData
est initialement la dernière valeur connue du client pour le chemin, ou nil
si aucune valeur n'est disponible. Le serveur compare la valeur initiale à sa valeur actuelle et accepte la transaction si les valeurs correspondent, ou la refuse. Si la transaction est refusée, le serveur renvoie la valeur actuelle au client, qui exécute à nouveau la transaction avec la valeur mise à jour. Cette opération se répète jusqu'à ce que la transaction soit acceptée ou que trop de tentatives aient été effectuées.
Incréments atomiques côté serveur
Dans le cas d'utilisation ci-dessus, nous écrivons deux valeurs dans la base de données : l'ID de l'utilisateur qui ajoute ou supprime l'étoile de l'article, et le nombre d'étoiles incrémenté. Si nous savons déjà que l'utilisateur a ajouté le post à ses favoris, nous pouvons utiliser une opération d'incrémentation atomique au lieu d'une transaction.
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];
Ce code n'utilise pas d'opération de transaction. Il n'est donc pas réexécuté automatiquement en cas de mise à jour conflictuelle. Toutefois, comme l'opération d'incrémentation se produit directement sur le serveur de base de données, il n'y a aucun risque de conflit.
Si vous souhaitez détecter et refuser les conflits spécifiques à l'application, par exemple lorsqu'un utilisateur ajoute une étoile à un post auquel il en a déjà ajouté une, vous devez écrire des règles de sécurité personnalisées pour ce cas d'utilisation.
Utiliser les données hors connexion
Si un client perd sa connexion réseau, votre application continuera de fonctionner correctement.
Chaque client connecté à une base de données Firebase conserve sa propre version interne des données actives. Lorsque des données sont écrites, elles le sont d'abord dans cette version locale. Le client Firebase synchronise ensuite ces données avec les serveurs de base de données à distance et avec d'autres clients dans la mesure du possible.
Par conséquent, toutes les écritures dans la base de données déclenchent immédiatement des événements locaux, avant que les données ne soient écrites sur le serveur. Cela signifie que votre application reste réactive, quelle que soit la latence ou la connectivité du réseau.
Une fois la connectivité rétablie, votre application reçoit l'ensemble d'événements approprié afin que le client se synchronise avec l'état actuel du serveur, sans avoir à écrire de code personnalisé.
Nous aborderons plus en détail le comportement hors connexion dans En savoir plus sur les fonctionnalités en ligne et hors connexion.