Ce document aborde 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 FirebaseDatabase
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.
(Facultatif) Prototypage et test avec Firebase Local Emulator Suite
Avant de voir comment votre application lit et écrit dans Realtime Database, présentons un ensemble d'outils que vous pouvez utiliser pour prototyper et tester les fonctionnalités Realtime Database : Firebase Local Emulator Suite. Si vous testez différents modèles de données, si vous optimisez vos règles de sécurité ou si vous cherchez le moyen le plus rentable d'interagir avec le backend, il peut être judicieux de pouvoir travailler localement sans déployer de services actifs.
Un émulateur Realtime Database fait partie de Local Emulator Suite, ce qui permet à votre application d'interagir avec le contenu et la configuration de votre base de données émulée, ainsi que, éventuellement, avec vos ressources de projet émulées (fonctions, autres bases de données et règles de sécurité).
Pour utiliser l'émulateur Realtime Database, procédez comme suit :
- 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
. - Effectuer des appels à partir du code de 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é impliquant Realtime Database et Cloud Functions est disponible. Nous vous conseillons également de consulter la présentation de Local 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
:
Kotlin+KTX
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Écrire des données
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:
- Types de cartes qui correspondent aux types JSON disponibles comme suit :
String
Long
Double
Boolean
Map<String, Object>
List<Object>
- Transmettez un objet Java personnalisé si la classe qui le définit possède un constructeur par défaut qui n'accepte aucun argument et dispose de getters publics pour les propriétés à attribuer.
Si vous utilisez un objet Java, le contenu de votre objet est automatiquement mappé à des emplacements enfants de manière imbriquée. De plus, l'utilisation d'un objet Java rend généralement votre code plus lisible et plus facile à gérer. Par exemple, si vous disposez d'une application avec un profil utilisateur de base, votre objet User
peut ressembler à ceci:
Kotlin+KTX
@IgnoreExtraProperties data class User(val username: String? = null, val email: String? = null) { // Null default values create a no-argument default constructor, which is needed // for deserialization from a DataSnapshot. }
Java
@IgnoreExtraProperties public class User { public String username; public String email; public User() { // Default constructor required for calls to DataSnapshot.getValue(User.class) } public User(String username, String email) { this.username = username; this.email = email; } }
Vous pouvez ajouter un utilisateur avec setValue()
comme suit:
Kotlin+KTX
fun writeNewUser(userId: String, name: String, email: String) { val user = User(name, email) database.child("users").child(userId).setValue(user) }
Java
public void writeNewUser(String userId, String name, String email) { User user = new User(name, email); mDatabase.child("users").child(userId).setValue(user); }
L'utilisation de setValue()
de cette manière écrase les données à l'emplacement spécifié, y compris 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 à mettre à jour leur profil, vous pouvez modifier le nom d'utilisateur comme suit:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Lire des données
Lire des données avec des écouteurs persistants
Pour lire des données à un chemin d'accès et écouter les modifications, utilisez la méthode addValueEventListener()
pour ajouter un ValueEventListener
à un DatabaseReference
.
Écouteur | Rappel d'événement | Utilisation type |
---|---|---|
ValueEventListener |
onDataChange() |
Lire et écouter les modifications apportées à l'ensemble du contenu d'un chemin. |
Vous pouvez utiliser la méthode onDataChange()
pour lire un instantané statique du contenu à un chemin d'accès donné, tel qu'il existait 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. Le rappel d'événement reçoit un instantané contenant toutes les données à cet emplacement, y compris les données enfants. Si aucune donnée n'est disponible, l'instantané renvoie false
lorsque vous appelez exists()
et null
lorsque vous appelez getValue()
.
L'exemple suivant illustre une application de blog sur les réseaux sociaux qui récupère les détails d'un article à partir de la base de données:
Kotlin+KTX
val postListener = object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { // Get Post object and use the values to update the UI val post = dataSnapshot.getValue<Post>() // ... } override fun onCancelled(databaseError: DatabaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()) } } postReference.addValueEventListener(postListener)
Java
ValueEventListener postListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // Get Post object and use the values to update the UI Post post = dataSnapshot.getValue(Post.class); // .. } @Override public void onCancelled(DatabaseError databaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()); } }; mPostReference.addValueEventListener(postListener);
L'écouteur reçoit un DataSnapshot
contenant les données à l'emplacement spécifié dans la base de données au moment de l'événement. L'appel de getValue()
sur un instantané renvoie la représentation de l'objet Java des données. Si aucune donnée n'existe à l'emplacement, l'appel de getValue()
renvoie null
.
Dans cet exemple, ValueEventListener
définit également la méthode onCancelled()
appelée si la lecture est annulée. Par exemple, une lecture peut être annulée si le client n'est pas autorisé à lire à partir d'un emplacement de base de données Firebase. Cette méthode reçoit un objet DatabaseError
indiquant pourquoi l'échec s'est produit.
Lire les données une fois
Lire une seule fois à l'aide de get()
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 général, vous devez utiliser les techniques ValueEventListener
décrites ci-dessus pour lire les données afin d'être informé des mises à jour des données à partir du backend. Les techniques d'écouteur 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 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 sonde le cache de stockage local et renvoie une erreur si la valeur n'est toujours pas trouvée.
Une utilisation inutile de get()
peut augmenter l'utilisation de la bande passante et entraîner une perte de performances. Vous pouvez l'éviter en utilisant un écouteur en temps réel, comme indiqué ci-dessus.
Kotlin+KTX
mDatabase.child("users").child(userId).get().addOnSuccessListener {
Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
Log.e("firebase", "Error getting data", it)
}
Java
mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
@Override
public void onComplete(@NonNull Task<DataSnapshot> task) {
if (!task.isSuccessful()) {
Log.e("firebase", "Error getting data", task.getException());
}
else {
Log.d("firebase", String.valueOf(task.getResult().getValue()));
}
}
});
Lire une fois à l'aide d'un écouteur
Dans certains cas, vous pouvez souhaiter que la valeur du cache local soit renvoyée immédiatement, au lieu de rechercher une valeur mise à jour sur le serveur. Dans ce cas, vous pouvez utiliser addListenerForSingleValueEvent
pour récupérer immédiatement les données du cache du disque local.
Cela 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 bloggage des exemples précédents utilise cette méthode pour charger le profil d'un utilisateur lorsqu'il commence à rédiger un nouvel article.
Mettre à jour ou supprimer des 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 updateChildren()
.
Lorsque vous appelez updateChildren()
, 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 dans plusieurs emplacements pour améliorer leur scaling, vous pouvez mettre à jour toutes les instances de ces données à l'aide de la distribution ramifiée des données. Par exemple, une application de bloggage sur les réseaux sociaux peut avoir une classe Post
comme suit:
Kotlin+KTX
@IgnoreExtraProperties data class Post( var uid: String? = "", var author: String? = "", var title: String? = "", var body: String? = "", var starCount: Int = 0, var stars: MutableMap<String, Boolean> = HashMap(), ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "uid" to uid, "author" to author, "title" to title, "body" to body, "starCount" to starCount, "stars" to stars, ) } }
Java
@IgnoreExtraProperties public class Post { public String uid; public String author; public String title; public String body; public int starCount = 0; public Map<String, Boolean> stars = new HashMap<>(); public Post() { // Default constructor required for calls to DataSnapshot.getValue(Post.class) } public Post(String uid, String author, String title, String body) { this.uid = uid; this.author = author; this.title = title; this.body = body; } @Exclude public Map<String, Object> toMap() { HashMap<String, Object> result = new HashMap<>(); result.put("uid", uid); result.put("author", author); result.put("title", title); result.put("body", body); result.put("starCount", starCount); result.put("stars", stars); return result; } }
Pour créer un post et le mettre à jour simultanément dans le flux d'activités récentes et le flux d'activités de l'utilisateur qui publie le post, l'application de blog utilise un code semblable à celui-ci:
Kotlin+KTX
private fun writeNewPost(userId: String, username: String, title: String, body: String) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously val key = database.child("posts").push().key if (key == null) { Log.w(TAG, "Couldn't get push key for posts") return } val post = Post(userId, username, title, body) val postValues = post.toMap() val childUpdates = hashMapOf<String, Any>( "/posts/$key" to postValues, "/user-posts/$userId/$key" to postValues, ) database.updateChildren(childUpdates) }
Java
private void writeNewPost(String userId, String username, String title, String body) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously String key = mDatabase.child("posts").push().getKey(); Post post = new Post(userId, username, title, body); Map<String, Object> postValues = post.toMap(); Map<String, Object> childUpdates = new HashMap<>(); childUpdates.put("/posts/" + key, postValues); childUpdates.put("/user-posts/" + userId + "/" + key, postValues); mDatabase.updateChildren(childUpdates); }
Cet exemple utilise push()
pour créer un post dans le nœud contenant des posts pour tous les utilisateurs à /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 à /user-posts/$userid/$postid
.
À l'aide de ces chemins, vous pouvez effectuer des mises à jour simultanées à plusieurs emplacements de l'arborescence JSON avec un seul appel à updateChildren()
, comme dans cet exemple qui crée le nouveau post dans les deux emplacements. Les mises à jour simultanées effectuées de cette manière sont atomiques : toutes les mises à jour réussissent ou toutes échouent.
Ajouter un rappel de fin de session
Si vous souhaitez savoir quand vos données ont été validées, vous pouvez ajouter un écouteur d'achèvement. setValue()
et updateChildren()
acceptent un écouteur de fin facultatif qui est appelé lorsque l'écriture a été validée dans la base de données. Si l'appel a échoué, un objet d'erreur indiquant pourquoi l'échec s'est produit est transmis à l'écouteur.
Kotlin+KTX
database.child("users").child(userId).setValue(user) .addOnSuccessListener { // Write was successful! // ... } .addOnFailureListener { // Write failed // ... }
Java
mDatabase.child("users").child(userId).setValue(user) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Write was successful! // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Write failed // ... } });
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 en spécifiant null
comme valeur pour une autre opération d'écriture, telle que setValue()
ou updateChildren()
. Vous pouvez utiliser cette technique avec updateChildren()
pour supprimer plusieurs enfants en un seul appel d'API.
Dissocier les écouteurs
Vous pouvez supprimer les rappels en appelant la méthode removeEventListener()
sur la référence de votre base de données Firebase.
Si un écouteur a été ajouté plusieurs fois à un emplacement de données, il est appelé plusieurs fois pour chaque événement, et vous devez le dissocier le même nombre de fois pour le supprimer complètement.
L'appel de removeEventListener()
sur un écouteur parent ne supprime pas automatiquement les écouteurs enregistrés sur ses nœuds enfants. removeEventListener()
doit également être appelé sur tous les écouteurs enfants pour supprimer le rappel.
Enregistrer les données en tant que transactions
Lorsque vous travaillez avec des données pouvant être corrompues par des modifications simultanées, telles que des compteurs incrémentaux, vous pouvez utiliser une opération de transaction. Vous donnez à cette opération deux arguments: une fonction de mise à jour et un rappel de fin facultatif. La fonction update prend l'état actuel des données comme argument et renvoie l'état souhaité que vous souhaitez écrire. Si un autre client écrit à l'emplacement avant que votre nouvelle valeur ne soit écrite, votre fonction de mise à jour est appelée à nouveau 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 à ajouter et à supprimer des étoiles aux posts, et à suivre le nombre d'étoiles qu'un post a reçues comme suit :
Kotlin+KTX
private fun onStarClicked(postRef: DatabaseReference) { // ... postRef.runTransaction(object : Transaction.Handler { override fun doTransaction(mutableData: MutableData): Transaction.Result { val p = mutableData.getValue(Post::class.java) ?: return Transaction.success(mutableData) if (p.stars.containsKey(uid)) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1 p.stars.remove(uid) } else { // Star the post and add self to stars p.starCount = p.starCount + 1 p.stars[uid] = true } // Set value and report transaction success mutableData.value = p return Transaction.success(mutableData) } override fun onComplete( databaseError: DatabaseError?, committed: Boolean, currentData: DataSnapshot?, ) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError!!) } }) }
Java
private void onStarClicked(DatabaseReference postRef) { postRef.runTransaction(new Transaction.Handler() { @NonNull @Override public Transaction.Result doTransaction(@NonNull MutableData mutableData) { Post p = mutableData.getValue(Post.class); if (p == null) { return Transaction.success(mutableData); } if (p.stars.containsKey(getUid())) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1; p.stars.remove(getUid()); } else { // Star the post and add self to stars p.starCount = p.starCount + 1; p.stars.put(getUid(), true); } // Set value and report transaction success mutableData.setValue(p); return Transaction.success(mutableData); } @Override public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot currentData) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError); } }); }
L'utilisation d'une transaction empêche le nombre d'étoiles d'être incorrect si plusieurs utilisateurs ajoutent une étoile au même post en même temps ou si le client dispose de données obsolètes. 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/supprime une étoile à la publication et le nombre d'étoiles incrémenté. Si nous savons déjà que l'utilisateur ajoute le post à ses favoris, nous pouvons utiliser une opération d'incrémentation atomique au lieu d'une transaction.
Kotlin+KTX
private fun onStarClicked(uid: String, key: String) { val updates: MutableMap<String, Any> = hashMapOf( "posts/$key/stars/$uid" to true, "posts/$key/starCount" to ServerValue.increment(1), "user-posts/$uid/$key/stars/$uid" to true, "user-posts/$uid/$key/starCount" to ServerValue.increment(1), ) database.updateChildren(updates) }
Java
private void onStarClicked(String uid, String key) { Map<String, Object> updates = new HashMap<>(); updates.put("posts/"+key+"/stars/"+uid, true); updates.put("posts/"+key+"/starCount", ServerValue.increment(1)); updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true); updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1)); mDatabase.updateChildren(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 incompatible. 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 rejeter les conflits spécifiques à l'application, par exemple lorsqu'un utilisateur ajoute une étoile à un post auquel il a déjà ajouté une étoile, vous devez écrire des règles de sécurité personnalisées pour ce cas d'utilisation.
Utiliser des 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 gère sa propre version interne de toutes les données sur lesquelles des écouteurs sont utilisés ou qui doivent être synchronisées avec le serveur. Lorsque des données sont lues ou écrites, cette version locale des données est utilisée en premier. Le client Firebase synchronise ensuite ces données avec les serveurs de base de données distants et avec d'autres clients de manière "optimisée".
Par conséquent, toutes les écritures dans la base de données déclenchent immédiatement des événements locaux, avant toute interaction avec 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é pour que le client se synchronise avec l'état actuel du serveur, sans avoir à écrire de code personnalisé.
Nous parlerons plus en détail du comportement hors connexion dans la section En savoir plus sur les fonctionnalités en ligne et hors connexion.