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 FIRDatabaseReference
Para leer o escribir en la base de datos, necesitas una instancia de FIRDatabaseReference
:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Escribe datos
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 Database
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.
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:
NSString
NSNumber
NSDictionary
NSArray
Por ejemplo, puedes agregar un usuario con setValue
como se muestra a continuación:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
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:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Lee datos
Lee datos detectando eventos de valor
Si quieres leer datos en una ruta de acceso y detectar cambios en ellos, usa el observeEventType:withBlock
de FIRDatabaseReference
a fin de observar eventos FIRDataEventTypeValue
.
Tipo de evento | Uso común |
---|---|
FIRDataEventTypeValue |
Lee y detecta cambios en el contenido de una ruta de acceso. |
Puedes usar el evento FIRDataEventTypeValue
para leer los datos de una ruta de acceso específica, tal como existen en el momento del evento. Este método se activa cuando se adjunta el objeto de escucha y se vuelve a activar cada vez que cambian los datos (incluidos los de segundo nivel). La devolución de llamada del evento recibe una snapshot
que contiene todos los datos de dicha
ubicación, incluidos los datos secundarios. Si no hay datos, la instantánea mostrará false
cuando llames a exists()
y nil
cuando leas su propiedad value
.
El siguiente ejemplo demuestra una aplicación de blogs sociales que recupera los detalles de una publicación de la base de datos:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
El objeto de escucha recibe una FIRDataSnapshot
que contiene los datos en la ubicación especificada en el momento del evento en su propiedad value
. Puedes asignar los valores al tipo nativo apropiado, como NSDictionary
.
Si no hay datos en la ubicación, el value
será nil
.
Lee los datos una sola vez
Realiza operaciones de lectura una sola vez con getData()
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 eventos de valor que se describieron antes a fin de leer datos para recibir notificaciones sobre las actualizaciones de los datos del backend. Estas técnicas reducen los costos de uso y 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 getData()
para obtener una instantánea de la base de datos. Si, por algún motivo, getData()
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.
En el siguiente ejemplo, se muestra cómo recuperar un nombre de usuario público una sola vez desde la base de datos:
Swift
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
Objective-C
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid]; [[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) { if (error) { NSLog(@"Received an error %@", error); return; } NSString *userName = snapshot.value; }];
El uso innecesario de getData()
puede aumentar la utilización del ancho de banda y reducir el rendimiento. Esto se puede evitar mediante un objeto de escucha en tiempo real, como se muestra arriba.
Lee datos una sola vez con un observador
En algunos casos, es recomendable que el valor de la caché local se muestre de inmediato, en lugar de buscar un valor actualizado en el servidor. En esos casos, puedes usar observeSingleEventOfType
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:
Swift
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) }
Objective-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); }];
Actualiza o borra datos
Actualiza campos específicos
Para escribir de forma simultánea en elementos secundarios específicos de un nodo sin sobrescribir otros nodos secundarios, usa el método updateChildValues
.
Cuando llamas a updateChildValues
, 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 mediante fan-out de datos. Por ejemplo, es posible que una app social de blogs desee crear una publicación y que esta aparezca en forma simultánea con el feed de actividad reciente y en el feed de actividad de las entradas del usuario. Para ello, la aplicación de blogs usa código similar al siguiente:
Swift
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)
Objective-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];
En este ejemplo, se usa childByAutoId
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 updateChildValues
, 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 un bloque de finalización
Si quieres saber en qué momento se confirma la escritura de los datos, puedes agregar un bloque de finalización. Tanto setValue
como updateChildValues
toman un bloque de finalización opcional que recibe una llamada cuando la escritura se confirma en la base de datos. El objeto de escucha es útil para mantener un registro de los datos que se guardan y de los que siguen en sincronización. Si la llamada no funciona correctamente, el agente de escucha recibirá un objeto de error que indicará el motivo.
Swift
do { try await ref.child("users").child(user.uid).setValue(["username": username]) print("Data saved successfully!") } catch { print("Data could not be saved: \(error).") }
Objective-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."); } }];
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 nil
como el valor de otra operación de escritura, como setValue
o updateChildValues
. Puedes usar esta técnica con updateChildValues
para borrar varios datos secundarios con una sola llamada a la API.
Desvincula objetos de escucha
Los observadores no detienen de forma automática la sincronización de datos cuando dejas un ViewController
. Si un observador no se quita de forma apropiada, la sincronización de datos continúa en la memoria local. Cuando ya no necesitas un observador, pasa el FIRDatabaseHandle
asociado al método removeObserverWithHandle
para quitarlo.
Cuando agregas un bloque de devoluciones de llamada a una referencia, se muestra FIRDatabaseHandle
.
Estos controladores se pueden usar para quitar el bloque de devoluciones de llamada.
Si se agregaron varios objetos de escucha a la referencia de una base de datos, se llama a cada objeto de escucha cuando surge un evento. Para detener la sincronización de datos en esa ubicación, debes quitar todos los observadores de la ubicación a través del método removeAllObservers
.
Llamar a removeObserverWithHandle
o a removeAllObservers
en un objeto de escucha no quita de manera automática los objetos de escucha registrados en estos nodos secundarios. De todos modos, debes hacer un seguimiento de esas referencias o controladores para quitarlos.
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.
Por ejemplo, en la app social de blogs que mencionamos antes, podrías permitir que los usuarios agreguen o quiten estrellas en las entradas y llevar un seguimiento de cuántas estrellas recibió una entrada de la siguiente forma:
Swift
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) } }
Objective-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); } }];
Si usas una transacción, evitas que el recuento de estrellas sea incorrecto en caso de que varios
usuarios agreguen estrellas a la entrada al mismo tiempo o el cliente tenga datos inactivos. Por lo general, el valor que contiene inicialmente la clase FIRMutableData
es el último valor conocido del cliente para la ruta de acceso, o nil
si no hay uno. El servidor compara el
valor inicial con el valor actual y acepta la transacción si los
valores coinciden. De lo contrario, la rechaza. 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 publicación, podemos usar una operación de incremento atómico en vez de una transacción.
Swift
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)
Objective-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];
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.
Todos los clientes conectados a una base de datos de Firebase mantienen su propia versión interna de los datos activos. Cuando se escriben datos, se hace primero en esta versión local. Después, el cliente de Firebase sincroniza esos 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 al instante, antes de que se escriban datos en 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