Lire et écrire des données

(Facultatif) Prototyper et tester avec Firebase 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 les fonctionnalités de la base de données en temps réel : Firebase 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 d'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é).emulator_suite_short

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. À partir de la racine du répertoire de votre projet local, exécutez firebase emulators:start .
  3. Effectuer des appels à partir du code prototype de votre application à l'aide d'un SDK de plateforme Realtime Database comme d'habitude ou à l'aide de l'API REST Realtime Database.

Une présentation détaillée impliquant la base de données en temps réel et les fonctions cloud est disponible. Vous devriez également jeter un œil à l’introduction d’Emulator Suite .

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

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

DatabaseReference ref = FirebaseDatabase.instance.ref();

Écrire des données

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

Les données Firebase sont écrites dans une DatabaseReference et récupérées en attendant ou en écoutant les événements émis par la référence. Les événements sont émis 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 set() pour enregistrer les données dans une référence spécifiée, en remplaçant toutes les données existantes sur ce chemin. Vous pouvez définir une référence aux types suivants : String , boolean , int , double , Map , List .

Par exemple, vous pouvez ajouter un utilisateur avec set() comme suit :

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

Utiliser set() de cette manière écrase 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 leur profil, vous pouvez mettre à jour le nom d'utilisateur comme suit :

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

// Only update the name, leave the age and address!
await ref.update({
  "age": 19,
});

La méthode update() accepte un sous-chemin vers les nœuds, vous permettant de mettre à jour plusieurs nœuds sur la base de données à la fois :

DatabaseReference ref = FirebaseDatabase.instance.ref("users");

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

Lire des données

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

Pour lire des données sur un chemin et écouter les modifications, utilisez la propriété onValue de DatabaseReference pour écouter les DatabaseEvent .

Vous pouvez utiliser DatabaseEvent pour lire les données à un chemin donné, telles qu'elles existent au moment de l'événement. Cet événement est déclenché une fois lorsque l'écouteur est attaché et à nouveau chaque fois que les données, y compris les enfants, changent. L'événement possède une propriété snapshot contenant toutes les données à cet emplacement, y compris les données enfants. S'il n'y a pas de données, la propriété exists de l'instantané sera false et sa propriété value sera nulle.

L'exemple suivant montre une application de blog social récupérant les détails d'une publication de la base de données :

DatabaseReference starCountRef =
        FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
    final data = event.snapshot.value;
    updateStarCount(data);
});

L'écouteur reçoit un DataSnapshot 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 .

Lire les données une fois

Lire une fois en utilisant get()

Le SDK est conçu pour gérer les interactions avec les serveurs de bases 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 et être averti des mises à jour des données depuis le 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 hors ligne.

Si vous n'avez besoin des données qu'une seule fois, vous pouvez utiliser get() pour obtenir un instantané des données de la base de données. Si, pour une raison quelconque get() ne parvient pas à renvoyer la valeur du serveur, le client sondera le cache de stockage local et renverra une erreur si la valeur n'est toujours pas trouvée.

L'exemple suivant montre la récupération unique du nom d'utilisateur public d'un utilisateur à partir de la base de données :

final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
    print(snapshot.value);
} else {
    print('No data available.');
}

L'utilisation inutile de get() 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 fois avec once()

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 once() pour obtenir immédiatement les données du cache disque local.

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 ni nécessiter une écoute active. Par exemple, l'application de blog dans les exemples précédents utilise cette méthode pour charger le profil d'un utilisateur lorsqu'il commence à rédiger un nouveau message :

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

Mise à jour ou suppression de données

Mettre à jour des champs spécifiques

Pour écrire simultanément sur des enfants spécifiques d'un nœud sans écraser les autres nœuds enfants, utilisez la méthode update() .

Lorsque vous appelez update() , vous pouvez mettre à jour les valeurs enfants de niveau inférieur en spécifiant un chemin pour la clé. Si les données sont stockées à plusieurs emplacements pour mieux évoluer, vous pouvez mettre à jour toutes les instances de ces données à l'aide de la répartition des données . Par exemple, une application de blog social peut souhaiter créer une publication et la mettre simultanément à jour avec le flux d'activité récente et le flux d'activité de l'utilisateur qui a publié la publication. Pour ce faire, l'application de blogging utilise un code comme celui-ci :

void writeNewPost(String uid, String username, String picture, String title,
        String body) async {
    // A post entry.
    final postData = {
        'author': username,
        'uid': uid,
        'body': body,
        'title': title,
        'starCount': 0,
        'authorPic': picture,
    };

    // Get a key for a new Post.
    final newPostKey =
        FirebaseDatabase.instance.ref().child('posts').push().key;

    // Write the new post's data simultaneously in the posts list and the
    // user's post list.
    final Map<String, Map> updates = {};
    updates['/posts/$newPostKey'] = postData;
    updates['/user-posts/$uid/$newPostKey'] = postData;

    return FirebaseDatabase.instance.ref().update(updates);
}

Cet exemple utilise push() pour créer une publication dans le nœud contenant des publications pour tous les utilisateurs de /posts/$postid et récupérer simultanément la clé avec key . La clé peut ensuite être utilisée pour créer une deuxième entrée dans les publications de l'utilisateur à /user-posts/$userid/$postid .

En utilisant ces chemins, vous pouvez effectuer des mises à jour simultanées vers plusieurs emplacements de l'arborescence JSON avec un seul appel à update() , par exemple comment cet exemple crée la nouvelle publication 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 les mises à jour échouent.

Ajouter un rappel d'achèvement

Si vous souhaitez savoir quand vos données ont été validées, vous pouvez enregistrer des rappels d'achèvement. set() et update() renvoient tous deux Future s, auxquels vous pouvez attacher des rappels de réussite et d'erreur qui sont appelés lorsque l'écriture a été validée dans la base de données et lorsque l'appel a échoué.

FirebaseDatabase.instance
    .ref('users/$userId/email')
    .set(emailAddress)
    .then((_) {
        // Data saved successfully!
    })
    .catchError((error) {
        // The write failed...
    });

Suprimmer les données

Le moyen le plus simple de supprimer des données consiste à appeler remove() sur une référence à l'emplacement de ces données.

Vous pouvez également supprimer en spécifiant null comme valeur pour une autre opération d'écriture telle que set() ou update() . Vous pouvez utiliser cette technique avec update() pour supprimer plusieurs enfants en un seul appel API.

Enregistrer les données en tant que transactions

Lorsque vous travaillez avec des données susceptibles d'être corrompues par des modifications simultanées, telles que des compteurs incrémentiels, vous pouvez utiliser une transaction en transmettant un gestionnaire de transaction à runTransaction() . Un gestionnaire de transactions prend l'état actuel des données comme argument et renvoie le nouvel état souhaité que vous souhaitez écrire. Si un autre client écrit à l'emplacement avant que votre nouvelle valeur ne soit écrite avec succès, votre fonction de mise à jour est à nouveau appelée avec la nouvelle valeur actuelle et l'écriture est réessayée.

Par exemple, dans l'exemple d'application de blog social, vous pouvez autoriser les utilisateurs à suivre et à supprimer des publications et à suivre le nombre d'étoiles qu'une publication a reçue comme suit :

void toggleStar(String uid) async {
  DatabaseReference postRef =
      FirebaseDatabase.instance.ref("posts/foo-bar-123");

  TransactionResult result = await postRef.runTransaction((Object? post) {
    // Ensure a post at the ref exists.
    if (post == null) {
      return Transaction.abort();
    }

    Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
    if (_post["stars"] is Map && _post["stars"][uid] != null) {
      _post["starCount"] = (_post["starCount"] ?? 1) - 1;
      _post["stars"][uid] = null;
    } else {
      _post["starCount"] = (_post["starCount"] ?? 0) + 1;
      if (!_post.containsKey("stars")) {
        _post["stars"] = {};
      }
      _post["stars"][uid] = true;
    }

    // Return the new data.
    return Transaction.success(_post);
  });
}

Par défaut, des événements sont déclenchés à chaque exécution de la fonction de mise à jour de transaction. Si vous exécutez la fonction plusieurs fois, vous pouvez voir des états intermédiaires. Vous pouvez définir applyLocally sur false pour supprimer ces états intermédiaires et attendre que la transaction soit terminée avant que les événements ne soient déclenchés :

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

Le résultat d'une transaction est un TransactionResult , qui contient des informations telles que si la transaction a été validée et le nouvel instantané :

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");

TransactionResult result = await ref.runTransaction((Object? post) {
  // ...
});

print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot

Annuler une transaction

Si vous souhaitez annuler une transaction en toute sécurité, appelez Transaction.abort() pour lancer une AbortTransactionException :

TransactionResult result = await ref.runTransaction((Object? user) {
  if (user !== null) {
    return Transaction.abort();
  }

  // ...
});

print(result.committed); // false

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 marque/désactive la publication et le nombre d'étoiles incrémenté. Si nous savons déjà que cet utilisateur met en vedette la publication, nous pouvons utiliser une opération d'incrémentation atomique au lieu d'une transaction.

void addStar(uid, key) async {
  Map<String, Object?> updates = {};
  updates["posts/$key/stars/$uid"] = true;
  updates["posts/$key/starCount"] = ServerValue.increment(1);
  updates["user-posts/$key/stars/$uid"] = true;
  updates["user-posts/$key/starCount"] = ServerValue.increment(1);
  return FirebaseDatabase.instance.ref().update(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 s'effectue 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à mise en vedette 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 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 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 parlerons davantage du comportement hors ligne dans En savoir plus sur les fonctionnalités en ligne et hors ligne .

Prochaines étapes