Este documento cubre los conceptos básicos de lectura y escritura de datos de Firebase.
Los datos de Firebase se escriben en una referencia FirebaseDatabase
y se recuperan adjuntando un detector asincrónico a la referencia. El oyente se activa una vez para el estado inicial de los datos y nuevamente cada vez que cambian los datos.
(Opcional) Prototipo y prueba con Firebase Local Emulator Suite
Antes de hablar sobre cómo su aplicación lee y escribe en Realtime Database, presentemos un conjunto de herramientas que puede usar para crear prototipos y probar la funcionalidad de Realtime Database: Firebase Local Emulator Suite. Si está probando diferentes modelos de datos, optimizando sus reglas de seguridad o trabajando para encontrar la forma más rentable de interactuar con el back-end, poder trabajar localmente sin implementar servicios en vivo puede ser una gran idea.
Un emulador de base de datos en tiempo real es parte de Local Emulator Suite, que permite que su aplicación interactúe con el contenido y la configuración de su base de datos emulada, así como, opcionalmente, con los recursos de su proyecto emulado (funciones, otras bases de datos y reglas de seguridad).
Usar el emulador de Realtime Database implica solo unos pocos pasos:
- Agregar una línea de código a la configuración de prueba de su aplicación para conectarse al emulador.
- Desde la raíz del directorio de su proyecto local, ejecute
firebase emulators:start
. - Realizar llamadas desde el código prototipo de su aplicación usando un SDK de plataforma Realtime Database como de costumbre, o usando la API REST de Realtime Database.
Está disponible un tutorial detallado sobre la base de datos en tiempo real y las funciones de la nube . También deberías echar un vistazo a la introducción a Local Emulator Suite .
Obtener una referencia de base de datos
Para leer o escribir datos de la base de datos, necesita una instancia de DatabaseReference
:
Kotlin+KTX
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Escribir datos
Operaciones básicas de escritura
Para operaciones de escritura básicas, puede usar setValue()
para guardar datos en una referencia específica, reemplazando cualquier dato existente en esa ruta. Puede utilizar este método para:
- Tipos de pase que corresponden a los tipos JSON disponibles de la siguiente manera:
-
String
-
Long
-
Double
-
Boolean
-
Map<String, Object>
-
List<Object>
-
- Pase un objeto Java personalizado, si la clase que lo define tiene un constructor predeterminado que no acepta argumentos y tiene captadores públicos para las propiedades que se asignarán.
Si utiliza un objeto Java, el contenido de su objeto se asigna automáticamente a ubicaciones secundarias de forma anidada. El uso de un objeto Java también suele hacer que el código sea más legible y más fácil de mantener. Por ejemplo, si tiene una aplicación con un perfil de usuario básico, su objeto User
podría tener el siguiente aspecto:
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; } }
Puede agregar un usuario con setValue()
de la siguiente manera:
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); }
El uso setValue()
de esta manera sobrescribe los datos en la ubicación especificada, incluidos los nodos secundarios. Sin embargo, aún puedes actualizar un elemento secundario sin tener que reescribir todo el objeto. Si desea permitir que los usuarios actualicen sus perfiles, puede actualizar el nombre de usuario de la siguiente manera:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
leer datos
Leer datos con oyentes persistentes
Para leer datos en una ruta y escuchar cambios, use el método addValueEventListener()
para agregar un ValueEventListener
a una DatabaseReference
.
Oyente | Devolución de llamada de evento | Uso típico |
---|---|---|
ValueEventListener | onDataChange() | Lea y escuche los cambios en todo el contenido de una ruta. |
Puede utilizar el método onDataChange()
para leer una instantánea estática del contenido en una ruta determinada, tal como existía en el momento del evento. Este método se activa una vez cuando se adjunta el oyente y nuevamente cada vez que cambian los datos, incluidos los secundarios. A la devolución de llamada del evento se le pasa una instantánea que contiene todos los datos en esa ubicación, incluidos los datos secundarios. Si no hay datos, la instantánea devolverá false
cuando llame exists()
y null
cuando llame a getValue()
.
El siguiente ejemplo demuestra una aplicación de blogs sociales 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 oyente recibe una DataSnapshot
que contiene los datos en la ubicación especificada en la base de datos en el momento del evento. Llamar a getValue()
en una instantánea devuelve la representación del objeto Java de los datos. Si no existen datos en la ubicación, llamar getValue()
devuelve null
.
En este ejemplo, ValueEventListener
también define el método onCancelled()
que se llama si se cancela la lectura. Por ejemplo, una lectura se puede cancelar si el cliente no tiene permiso para leer desde una ubicación de base de datos de Firebase. A este método se le pasa un objeto DatabaseError
que indica por qué se produjo el error.
Leer datos una vez
Leer una vez usando get()
El SDK está diseñado para gestionar interacciones con servidores de bases de datos, ya sea que su aplicación esté en línea o fuera de línea.
Generalmente, debe utilizar las técnicas ValueEventListener
descritas anteriormente para leer datos y recibir notificaciones sobre las actualizaciones de los datos desde el backend. Las técnicas de escucha reducen su uso y facturación y están optimizadas para brindar a sus usuarios la mejor experiencia cuando se conectan y desconectan.
Si necesita los datos solo una vez, puede usar get()
para obtener una instantánea de los datos de la base de datos. Si por algún motivo get()
no puede devolver el valor del servidor, el cliente sondeará el caché de almacenamiento local y devolverá un error si aún no se encuentra el valor.
El uso innecesario de get()
puede aumentar el uso del ancho de banda y provocar una pérdida de rendimiento, lo que se puede evitar utilizando un 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()));
}
}
});
Leer una vez usando un oyente
En algunos casos, es posible que desee que el valor de la caché local se devuelva inmediatamente, en lugar de buscar un valor actualizado en el servidor. En esos casos, puede utilizar addListenerForSingleValueEvent
para obtener los datos del caché del disco local inmediatamente.
Esto es útil para datos que solo deben cargarse una vez y no se espera que cambien con frecuencia ni requieran una escucha activa. Por ejemplo, la aplicación de blogs de los ejemplos anteriores utiliza este método para cargar el perfil de un usuario cuando comienza a escribir una nueva publicación.
Actualizar o eliminar datos
Actualizar campos específicos
Para escribir simultáneamente en hijos específicos de un nodo sin sobrescribir otros nodos hijos, utilice el método updateChildren()
.
Al llamar updateChildren()
, puede actualizar los valores secundarios de nivel inferior especificando una ruta para la clave. Si los datos se almacenan en varias ubicaciones para escalar mejor, puede actualizar todas las instancias de esos datos mediante la distribución en abanico de datos . Por ejemplo, una aplicación de blogs sociales podría tener una clase Post
como esta:
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 simultáneamente a la fuente de actividad reciente y a la fuente de actividad del usuario que publica, 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); }
Este ejemplo usa push()
para crear una publicación en el nodo que contiene publicaciones para todos los usuarios en /posts/$postid
y simultáneamente recupera la clave con getKey()
. Luego, la clave se puede usar para crear una segunda entrada en las publicaciones del usuario en /user-posts/$userid/$postid
.
Con estas rutas, puede realizar actualizaciones simultáneas en varias ubicaciones en el árbol JSON con una sola llamada a updateChildren()
, como en este ejemplo se crea la nueva publicación en ambas ubicaciones. Las actualizaciones simultáneas realizadas de esta manera son atómicas: o todas las actualizaciones tienen éxito o todas fallan.
Agregar una devolución de llamada de finalización
Si desea saber cuándo se confirmaron sus datos, puede agregar un detector de finalización. Tanto setValue()
como updateChildren()
toman un detector de finalización opcional que se llama cuando la escritura se ha confirmado correctamente en la base de datos. Si la llamada no tuvo éxito, se pasa al oyente un objeto de error que indica por qué se produjo el error.
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 // ... } });
Borrar datos
La forma más sencilla de eliminar datos es llamar removeValue()
en una referencia a la ubicación de esos datos.
También puede eliminar especificando null
como valor para otra operación de escritura como setValue()
o updateChildren()
. Puede utilizar esta técnica con updateChildren()
para eliminar varios elementos secundarios en una única llamada API.
Separar oyentes
Las devoluciones de llamada se eliminan llamando al método removeEventListener()
en la referencia de tu base de datos de Firebase.
Si un oyente se ha agregado varias veces a una ubicación de datos, se llama varias veces para cada evento y debe separarlo la misma cantidad de veces para eliminarlo por completo.
Llamar removeEventListener()
en un oyente principal no elimina automáticamente los oyentes registrados en sus nodos secundarios; También se debe invocar removeEventListener()
en cualquier oyente secundario para eliminar la devolución de llamada.
Guardar datos como transacciones
Cuando trabaje con datos que podrían dañarse debido a modificaciones simultáneas, como contadores incrementales, puede utilizar una operación de transacción . A esta operación se le dan dos argumentos: una función de actualización y una devolución de llamada de finalización opcional. La función de actualización toma el estado actual de los datos como argumento y devuelve el nuevo estado deseado que le gustaría escribir. Si otro cliente escribe en la ubicación antes de que su nuevo valor se escriba correctamente, se llama nuevamente a su función de actualización con el nuevo valor actual y se vuelve a intentar la escritura.
Por ejemplo, en la aplicación de blogs sociales de ejemplo, podría permitir a los usuarios destacar y quitar publicaciones y realizar un seguimiento de cuántas estrellas ha recibido una publicación de la siguiente manera:
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); } }); }
El uso de una transacción evita que el recuento de estrellas sea incorrecto si varios usuarios destacan la misma publicación al mismo tiempo o si el cliente tenía datos obsoletos. Si la transacción es rechazada, el servidor devuelve el valor actual al cliente, que ejecuta la transacción nuevamente con el valor actualizado. Esto se repite hasta que se acepta la transacción o se han realizado demasiados intentos.
Incrementos atómicos del lado del servidor
En el caso de uso anterior, escribimos dos valores en la base de datos: el ID del usuario que destaca o quita la estrella de la publicación y el recuento de estrellas incrementado. Si ya sabemos que el usuario protagoniza la publicación, podemos usar una operación de incremento atómico en lugar 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 utiliza una operación de transacción, por lo que no se vuelve a ejecutar automáticamente si hay una actualización conflictiva. Sin embargo, dado que la operación de incremento ocurre directamente en el servidor de la base de datos, no hay posibilidad de que se produzca un conflicto.
Si desea detectar y rechazar conflictos específicos de una aplicación, como un usuario que destaca una publicación que ya destacó anteriormente, debe escribir reglas de seguridad personalizadas para ese caso de uso.
Trabajar con datos sin conexión
Si un cliente pierde su conexión de red, su aplicación seguirá funcionando correctamente.
Cada cliente conectado a una base de datos de Firebase mantiene su propia versión interna de cualquier dato en el que se estén utilizando oyentes o que esté marcado para mantenerse sincronizado con el servidor. Cuando se leen o escriben datos, esta versión local de los datos se utiliza primero. Luego, el cliente de Firebase sincroniza esos datos con los servidores de bases de datos remotos y con otros clientes al "mejor esfuerzo".
Como resultado, todas las escrituras en la base de datos desencadenan eventos locales inmediatamente, antes de cualquier interacción con el servidor. Esto significa que su aplicación sigue respondiendo independientemente de la latencia o la conectividad de la red.
Una vez que se restablece la conectividad, su aplicación recibe el conjunto apropiado de eventos para que el cliente se sincronice con el estado actual del servidor, sin tener que escribir ningún código personalizado.
Hablaremos más sobre el comportamiento fuera de línea en Más información sobre las capacidades en línea y fuera de línea .
Próximos pasos
- Trabajar con listas de datos
- Aprenda a estructurar datos
- Obtenga más información sobre las capacidades en línea y fuera de línea