Lire et écrire des données sur iOS

(Facultatif) Prototyper et tester avec Firebase Local Emulator Suite

Avant de parler de la façon dont votre application lit et écrit dans la base de données en temps réel, présentons un ensemble d'outils que vous pouvez utiliser pour prototyper et tester la fonctionnalité de la base de données en temps réel : 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 rentable d'interagir avec le back-end, pouvoir travailler localement sans déployer de services en direct peut être une excellente idée.

Un émulateur de base de données en temps réel fait partie de la suite d'émulateurs locaux, qui permet à votre application d'interagir avec le contenu et la configuration de votre base de données émulée, ainsi qu'éventuellement avec vos ressources de projet émulées (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 :

  1. Ajout d'une ligne de code à la configuration de test de votre application pour vous connecter à l'émulateur.
  2. A partir de la racine de votre répertoire local du projet, en cours d' exécution firebase emulators:start .
  3. Passer 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.

Une détaillée visite virtuelle en temps réel impliquant la base de données et les fonctions de Cloud est disponible. Vous devriez aussi jeter un oeil à l' adoption Emulator locale Suite .

Obtenir une référence de base de données FIR

Pour lire ou données d'écriture à partir de la base de données, vous avez besoin d' une instance de FIRDatabaseReference :

Rapide

var ref: DatabaseReference!

ref = Database.database().reference()

Objectif c

@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

Écrire des données

Ce document couvre les bases de la lecture et de l'écriture de données Firebase.

Données Firebase est écrit à une FIRDatabase référence et récupéré par la fixation d' un auditeur asynchrone par rapport à la référence. L'écouteur est déclenché une fois pour l'état initial des données et à nouveau 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 à une référence spécifiée, de remplacer toutes les données existantes sur ce chemin. Vous pouvez utiliser cette méthode pour :

  • Transmettez les types qui correspondent aux types JSON disponibles comme suit :
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

Par exemple, vous pouvez ajouter un utilisateur avec setValue comme suit:

Rapide

self.ref.child("users").child(user.uid).setValue(["username": username])

Objectif c

[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

L' utilisation setValue de cette manière écrasera les données à l'emplacement spécifié, y compris les nœuds enfants. Cependant, vous pouvez toujours mettre à jour un enfant sans réécrire l'intégralité de l'objet. Si vous souhaitez autoriser les utilisateurs à mettre à jour leurs profils, vous pouvez mettre à jour le nom d'utilisateur comme suit :

Rapide

self.ref.child("users/\(user.uid)/username").setValue(username)

Objectif c

[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

Lire les données

Lire les données en écoutant les événements de valeur

Pour lire les données sur un chemin et écouter les modifications, utilisez le observeEventType:withBlock de FIRDatabaseReference pour observer FIRDataEventTypeValue événements.

Type d'événement Utilisation typique
FIRDataEventTypeValue Lisez et écoutez les modifications apportées à l'ensemble du contenu d'un chemin.

Vous pouvez utiliser le FIRDataEventTypeValue événement pour lire les données à un chemin donné, tel qu'il existe au moment de l'événement. Cette méthode est déclenchée une fois lorsque l'écouteur est attaché et à nouveau chaque fois que les données, y compris les enfants, changent. Le rappel d'événement est passé un snapshot contenant toutes les données à cet endroit, y compris les données de l' enfant. S'il n'y a pas de données, l'instantané retourne false lorsque vous appelez exists() et nil lorsque vous lisez la value des biens.

L'exemple suivant illustre une application de blogs sociaux récupérant les détails d'une publication à partir de la base de données :

Rapide

refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objectif c

_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

L'auditeur 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 value propriété. Vous pouvez attribuer les valeurs du type natif approprié, comme NSDictionary . Si aucune donnée existe à l'emplacement, la value est nil .

Lire les données une fois

Lire une fois en utilisant 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 ligne.

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 afin d'être informé des mises à jour des données à partir 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 sont en ligne et hors ligne.

Si vous avez besoin des données qu'une seule fois, vous pouvez utiliser getData() pour obtenir un aperçu des données de la base de données. Si pour une raison quelconque getData() est incapable de retourner la valeur du serveur, le client sonde le cache de stockage local et renvoie une erreur si la valeur est toujours introuvable.

L' utilisation inutile de getData() peut augmenter l' utilisation de la bande passante et entraîner une perte de performance, qui peut être évité en utilisant un auditeur en temps réel comme indiqué ci - dessus.

self.ref.child("users/\(user.uid)/username").getData { (error, snapshot) in
    if let error = error {
        print("Error getting data \(error)")
    }
    else if snapshot.exists() {
        print("Got data \(snapshot.value!)")
    }
    else {
        print("No data available")
    }
}

Lire les données une fois avec un observateur

Dans certains cas, vous souhaiterez peut-être que la valeur du cache local soit renvoyée immédiatement, au lieu de rechercher une valeur mise à jour sur le serveur. Dans ces cas , vous pouvez utiliser observeSingleEventOfType pour obtenir les données du cache du disque local immédiatement.

Ceci est utile pour les données qui ne doivent être chargées qu'une seule fois et qui ne devraient pas changer fréquemment ou 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 nouveau message :

Rapide

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)
}

Objectif 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 aux enfants spécifiques d'un noeud sans écraser les autres nœuds enfants, utilisez la updateChildValues méthode.

Lors de l' appel updateChildValues , vous pouvez mettre à jour les valeurs des enfants de niveau inférieur en spécifiant un chemin pour la clé. Si les données sont stockées dans plusieurs endroits à l' échelle mieux, vous pouvez mettre à jour toutes les instances de ces données à l' aide de données sortance . Par exemple, une application de blogs sociaux peut vouloir créer une publication et la mettre à jour simultanément avec le fil d'activité récent et le fil d'activité de l'utilisateur publiant. Pour ce faire, l'application de blog utilise un code comme celui-ci :

Rapide

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)

Objectif 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 poste dans le nœud contenant les messages pour tous les utilisateurs à /posts/$postid et récupérer en même temps la clé avec getKey() . La clé peut être utilisée pour créer une deuxième entrée dans les messages de l'utilisateur à /user-posts/$userid/$postid .

L' utilisation de ces chemins, vous pouvez effectuer des mises à jour simultanées à plusieurs endroits dans l'arborescence JSON avec un seul appel à updateChildValues , comme la façon dont cet exemple crée le nouveau poste dans les deux endroits. Les mises à jour simultanées effectuées de cette manière sont atomiques : soit toutes les mises à jour réussissent, soit toutes les mises à jour échouent.

Ajouter un bloc d'achèvement

Si vous voulez savoir quand vos données ont été validées, vous pouvez ajouter un bloc d'achèvement. Les deux setValue et updateChildValues prennent un bloc d'achèvement en option qui est appelée lorsque l'écriture a été commise à la base de données. Cet écouteur peut être utile pour savoir quelles données ont été enregistrées et quelles données sont encore en cours de synchronisation. Si l'appel a échoué, l'auditeur reçoit un objet d'erreur indiquant pourquoi l'échec s'est produit.

Rapide

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!")
  }
}

Objectif 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.");
  }
}];

Suprimmer les données

La façon la plus simple de suppression de données est d'appeler removeValue sur une référence à l'emplacement de ces données.

Vous pouvez également supprimer en spécifiant nil comme valeur pour une autre opération d'écriture tels que setValue ou updateChildValues . Vous pouvez utiliser cette technique avec updateChildValues pour supprimer plusieurs enfants dans un seul appel API.

Détacher les auditeurs

Les observateurs ne s'arrêtent pas automatiquement la synchronisation des données lorsque vous quittez une ViewController . Si un observateur n'est pas correctement supprimé, il continue de synchroniser les données avec la mémoire locale. Lorsqu'un observateur n'est plus nécessaire, retirez - le en passant le associé FIRDatabaseHandle à la removeObserverWithHandle méthode.

Lorsque vous ajoutez un bloc de rappel à une référence, un FIRDatabaseHandle est retourné. Ces poignées peuvent être utilisées pour supprimer le bloc de rappel.

Si plusieurs écouteurs ont été ajoutés à une référence de base de données, chaque écouteur est appelé lorsqu'un événement est déclenché. Pour arrêter la synchronisation des données à cet endroit, vous devez supprimer tous les observateurs à un endroit en appelant la removeAllObservers méthode.

Appel 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 poignées pour les supprimer.

Enregistrer les données en tant que transactions

Lorsque vous travaillez avec des données qui pourraient être corrompues par des modifications simultanées telles que les compteurs supplémentaires, vous pouvez utiliser une opération de transaction . Vous donnez à cette opération deux arguments : 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'exemple d'application de blogs sociaux, vous pouvez autoriser les utilisateurs à afficher et à supprimer des articles et à suivre le nombre d'étoiles reçues par un article comme suit :

Rapide

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)
  }
}

Objectif 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 empêche le nombre d'étoiles d'être incorrect si plusieurs utilisateurs affichent le même message en même temps ou si le client avait des données périmées. La valeur contenue dans la FIRMutableData classe est initialement dernière valeur connue du client pour le chemin, ou nil s'il n'y en a pas. Le serveur compare la valeur initiale à sa valeur actuelle et accepte la transaction si les valeurs correspondent, ou la rejette. Si la transaction est rejetée, le serveur renvoie la valeur actuelle au client, qui exécute à nouveau la transaction avec la valeur mise à jour. Cela se répète jusqu'à ce que la transaction soit acceptée ou que trop de tentatives aient été effectuées.

Incréments côté serveur atomiques

Dans le cas d'utilisation ci-dessus, nous écrivons deux valeurs dans la base de données : l'ID de l'utilisateur qui affiche/annule la publication et le nombre d'étoiles incrémenté. Si nous savons déjà que l'utilisateur est la vedette de la publication, nous pouvons utiliser une opération d'incrémentation atomique au lieu d'une transaction.

Rapide

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);

Objectif 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 automatiquement réexécuté en cas de mise à jour conflictuelle. Cependant, étant donné que 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 rejeter les conflits spécifiques à une application, tels qu'un utilisateur mettant en vedette une publication qu'il a déjà publiée auparavant, vous devez écrire des règles de sécurité personnalisées pour ce cas d'utilisation.

Travailler avec des données hors ligne

Si un client perd sa connexion réseau, votre application continuera à fonctionner correctement.

Chaque client connecté à une base de données Firebase conserve sa propre version interne de toutes les données actives. Lorsque les données sont écrites, elles sont d'abord écrites dans cette version locale. Le client Firebase synchronise ensuite ces données avec les serveurs de base de données distants et avec d'autres clients au mieux.

Par conséquent, toutes les écritures dans la base de données déclenchent des événements locaux immédiatement, avant que des 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 allons parler plus sur le comportement en ligne En savoir plus sur les capacités en ligne et hors ligne .

Prochaines étapes