Este documento abarca los conceptos básicos de la lectura y la escritura de datos en Firebase.
Los datos de Firebase se escriben en una referencia de FirebaseDatabase
y para recuperarlos se debe adjuntar un objeto de escucha asíncrono a la referencia. El objeto de escucha se activa una vez para el estado inicial de los datos y otra vez cuando los datos cambian.
Crea prototipos y realiza pruebas con Firebase Local Emulator Suite (opcional)
Antes de analizar cómo la app realiza las operaciones de lectura y escritura en Realtime Database, veamos un conjunto de herramientas que puedes usar para crear prototipos y probar la funcionalidad de Realtime Database: Firebase Local Emulator Suite. Si quieres probar diferentes modelos de datos, optimizar tus reglas de seguridad o encontrar la forma más rentable de interactuar con el backend, recomendamos que trabajes a nivel local sin implementar servicios en ejecución.
Un emulador de Realtime Database forma parte de Local Emulator Suite, lo que permite que tu app interactúe con el contenido y la configuración emulados de la base de datos y, si lo deseas, con los recursos emulados del proyecto (funciones, otras bases de datos y reglas de seguridad).
El uso del emulador de Realtime Database solo requiere algunos pasos sencillos:
- Agrega una línea de código a la configuración de prueba de tu app para conectarte al emulador.
- Desde la raíz del directorio de tu proyecto local, ejecuta
firebase emulators:start
. - Realiza llamadas desde el código prototipo de tu app con un SDK de la plataforma de Realtime Database como de costumbre, o bien usa la API de REST de Realtime Database.
Hay una explicación detallada sobre Realtime Database y Cloud Functions disponible. También deberías consultar la introducción a Local Emulator Suite.
Obtén una DatabaseReference
Para leer o escribir en la base de datos, necesitas una instancia de
DatabaseReference
:
Kotlin+KTX
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Escribe datos
Operaciones básicas de escritura
Si quieres ejecutar operaciones básicas de escritura, puedes usar setValue()
para guardar datos en una referencia
específica y reemplazar todos los datos en esa ruta. Puedes usar este método para lo siguiente:
- Pasar tipos que corresponden a los tipos disponibles de JSON de la siguiente manera:
String
Long
Double
Boolean
Map<String, Object>
List<Object>
- Pasar un objeto Java personalizado, si la clase que lo define tiene un constructor predeterminado que no recibe argumentos y tiene métodos get públicos para que se asignen las propiedades
Si usas un objeto Java, el contenido de tu objeto se asigna automáticamente a las ubicaciones secundarias de forma anidada. Por lo general, el uso de un objeto Java hace que tu código sea más fácil de leer y mantener. Por ejemplo, si tienes una
app con un perfil básico de usuario, tu objeto User
podría ser similar al siguiente:
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; } }
Puedes agregar un usuario con setValue()
, como se muestra a continuación:
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); }
Si usas setValue()
de esta forma, se reemplazan los datos en la ubicación especificada,
incluidos los nodos secundarios. Sin embargo, es posible actualizar un elemento secundario sin volver a escribir el objeto entero. Si deseas permitir que los usuarios actualicen sus perfiles,
podrías actualizar el nombre de usuario de la siguiente forma:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Lee datos
Lee datos con objetos de escucha persistentes
Si quieres leer datos de una ruta de acceso y detectar los posibles cambios, usa el método addValueEventListener()
para agregar un ValueEventListener
a una DatabaseReference
.
Objeto de escucha | Devolución de llamada de evento | Uso común |
---|---|---|
ValueEventListener |
onDataChange() |
Lee y detecta cambios en el contenido de una ruta de acceso. |
Puedes usar el método onDataChange()
para leer una instantánea estática del contenido de una ruta de acceso determinada y ver cómo se encontraba en el momento del evento. Este método
se activa cuando se vincula el objeto de escucha y se vuelve a activar cada vez que cambian los datos,
incluidos los de nivel secundario. La devolución de llamada del evento recibe una instantánea que contiene todos los datos de esa ubicación, incluidos los datos secundarios. Si no hay datos, la instantánea mostrará el valor false
cuando llames a exists()
y null
cuando llames a getValue()
.
El siguiente ejemplo demuestra una aplicación social de blogs que recupera los detalles de una publicación de la base de datos:
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);
El objeto de escucha recibe una DataSnapshot
que contiene los datos de la ubicación específica
en la base de datos en el momento en que ocurrió el evento. Si llamas a getValue()
en una instantánea, se muestra la representación de los datos del objeto Java. Si no existen datos en la ubicación, la llamada a getValue()
muestra null
.
En este ejemplo, ValueEventListener
también define el método onCancelled()
que se llama si se cancela la lectura. Por ejemplo, se puede cancelar una lectura si el
cliente no tiene permiso para leer datos de una ubicación en la base de datos de Firebase. Este método recibe un objeto DatabaseError
que indica por qué se produjo el error.
Lee los datos una sola vez
Realiza operaciones de lectura una vez con get()
El SDK se diseñó para administrar interacciones con servidores de bases de datos, sin importar si tu app está en línea o sin conexión.
En general, debes usar las técnicas de ValueEventListener
que se describieron antes para leer datos y recibir notificaciones sobre las actualizaciones de los datos del backend. Las técnicas de los objetos de escucha reducen el uso y la facturación, y están optimizadas para brindar a los usuarios la mejor experiencia en línea y sin conexión.
Si necesitas los datos solo una vez, puedes usar get()
para obtener una instantánea de la base de datos. Si, por algún motivo, get()
no puede mostrar el valor del servidor, el cliente sondeará la caché de almacenamiento local y mostrará un error si de todos modos no encuentra el valor.
El uso innecesario de get()
puede aumentar la utilización del ancho de banda y reducir el
rendimiento. Esto se puede evitar usando un objeto de escucha en tiempo real, como se muestra arriba.
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()));
}
}
});
Realiza una operación de lectura una vez con un objeto de escucha
En algunos casos, es recomendable que se muestre de inmediato el valor de la caché local,
en lugar de buscar un valor actualizado en el servidor. En esos casos, puedes usar addListenerForSingleValueEvent
para obtener los datos de la memoria caché del disco local inmediatamente.
Esto resulta útil para los datos que solo se deben cargar una vez y que no se espera que cambien con frecuencia ni necesiten una escucha activa. Por ejemplo, en la app de blogs de los ejemplos anteriores se usa este método para cargar el perfil de un usuario cuando este comienza a crear una publicación nueva.
Actualiza o borra datos
Actualiza campos específicos
Para escribir de forma simultánea en elementos secundarios específicos de un nodo sin reemplazar otros nodos secundarios, usa el método updateChildren()
.
Cuando llamas a updateChildren()
, puedes especificar una ruta de acceso de la clave para actualizar valores secundarios de nivel inferior. Si se almacenan datos en varias ubicaciones para obtener un mejor escalamiento, puedes actualizar todas las instancias de esos datos usando fan-out de datos. Por ejemplo, una
app social de blogs podría tener una clase Post
como la siguiente:
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; } }
Para crear una publicación y actualizarla de forma simultánea con el feed de actividad reciente y el de actividad de las entradas del usuario, la aplicación de blogs usa un código como este:
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); }
En este ejemplo, se usa push()
para crear una entrada en el nodo que contiene las entradas de
todos los usuarios en /posts/$postid
y recuperar la clave con
getKey()
de manera simultánea. Luego, se puede usar la clave para crear una segunda entrada en las publicaciones del usuario en /user-posts/$userid/$postid
.
Con estas rutas de acceso, puedes ejecutar actualizaciones simultáneas en varias ubicaciones del árbol JSON con una única llamada a updateChildren()
, de manera similar a este ejemplo en el que se crea la publicación nueva en ambas ubicaciones. Las actualizaciones simultáneas que se hacen de esta forma son atómicas: todas se ejecutan correctamente o todas fallan.
Agrega una devolución de llamada de finalización
Si quieres saber en qué momento se confirma la escritura de los datos, puedes agregar un objeto de escucha de finalización. Tanto setValue()
como updateChildren()
toman un objeto de escucha de finalización opcional que se llama cuando la escritura se confirma con éxito en la base de datos. Si la llamada no funciona correctamente, el objeto de escucha
recibirá un objeto de error que indicará el motivo.
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 // ... } });
Borra datos
La forma más sencilla de borrar datos es llamar a removeValue()
en una referencia a la ubicación de los datos.
También puedes borrar datos si especificas null
como el valor de otra operación de escritura, como setValue()
o updateChildren()
. Puedes usar esta técnica con updateChildren()
para borrar varios datos secundarios con una sola llamada a la API.
Desvincula objetos de escucha
Para quitar las devoluciones de llamada, llama al método removeEventListener()
en tu referencia de la base de datos de Firebase.
Si un objeto de escucha se agregó varias veces a una ubicación de datos, se llama varias veces para cada evento y debes desvincularlo la misma cantidad de veces para quitarlo por completo.
Si llamas a removeEventListener()
en un objeto de escucha primario, no se quitan automáticamente los objetos de escucha registrados en sus nodos secundarios. Deberás llamar a removeEventListener()
también en todos los objetos de escucha secundarios para quitar la devolución de llamada.
Guarda datos como transacciones
Cuando trabajas con datos que se podrían dañar si se hacen cambios simultáneos (por ejemplo, contadores incrementales), puedes usar una operación de transacción. Esta operación acepta dos argumentos: una función de actualización y una devolución de llamada opcional de finalización. La función de actualización toma el estado actual de los datos como argumento y genera el nuevo estado que deseas escribir. Si otro cliente escribe en la ubicación antes de que se escriba de manera correcta el valor nuevo, se vuelve a llamar a la función de actualización con el nuevo valor actual y se intenta nuevamente la operación de escritura.
Por ejemplo, en la app social de blogs de ejemplo, podrías permitir que los usuarios destaquen o dejen de destacar las entradas y llevar un seguimiento de cuántas estrellas recibió una entrada de la siguiente forma:
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); } }); }
Cuando usas una transacción, evitas que el recuento de estrellas sea incorrecto en caso de que varios usuarios destaquen la entrada al mismo tiempo o el cliente tenga datos inactivos. Si se rechaza la transacción, el servidor muestra el valor actual al cliente, que vuelve a ejecutar la transacción con el valor actualizado. Esto se repite hasta que se acepte la transacción o hasta que se registren demasiados intentos.
Incrementos atómicos del servidor
En el caso de uso anterior, se escriben dos valores en la base de datos: el ID del usuario que destacó o dejó de destacar la entrada, y el recuento de estrellas general. Si ya sabemos que el usuario destacará la entrada, podemos usar una operación de incremento atómico en vez de una transacción.
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); }
Este código no usa una operación de transacción, por lo que no se vuelve a ejecutar automáticamente si hay una actualización conflictiva. Sin embargo, como la operación de incremento ocurre directamente en el servidor de la base de datos, no hay posibilidades de que ocurran conflictos.
Si quieres detectar y rechazar conflictos específicos de la aplicación (por ejemplo, si un usuario destaca una entrada que ya destacó antes), debes escribir reglas de seguridad personalizadas para ese caso de uso.
Trabaja con datos sin conexión
Si un cliente pierde la conexión de red, la app continúa funcionando de manera correcta.
Cada cliente conectado a una base de datos de Firebase conserva su propia versión interna de los datos en los que se usan los objetos de escucha o que se marcan para su sincronización con el servidor. Cuando se leen o escriben datos, primero se usa esta versión local. Después, el cliente de Firebase sincroniza los datos con los servidores de bases de datos remotas y con otros clientes según el “mejor esfuerzo”.
Como resultado, todas las operaciones de escritura en la base de datos activan eventos locales de inmediato, antes de que ocurra cualquier interacción con el servidor. Esto significa que la app conserva la capacidad de respuesta, sin importar la latencia o el estado de conexión de la red.
Cuando se restablece la conectividad, la app recibe el conjunto de eventos adecuado, de manera que el cliente se sincroniza con el estado actual del servidor sin tener que escribir código personalizado.
Obtén más detalles sobre el comportamiento sin conexión en Más información sobre las capacidades en línea y sin conexión.
Próximos pasos
- Trabaja con listas de datos
- Aprende a estructurar datos
- Más información sobre las capacidades en línea y sin conexión