Leer y escribir datos en plataformas Apple

(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:

  1. Agregar una línea de código a la configuración de prueba de su aplicación para conectarse al emulador.
  2. Desde la raíz del directorio de su proyecto local, ejecute firebase emulators:start .
  3. 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 FIR

Para leer o escribir datos de la base de datos, necesita una instancia de FIRDatabaseReference :

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
var ref: DatabaseReference!

ref = Database.database().reference()

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

Escribir datos

Este documento cubre los conceptos básicos de lectura y escritura de datos de Firebase.

Los datos de Firebase se escriben en una referencia Database 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.

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:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

Por ejemplo, puedes agregar un usuario con setValue de la siguiente manera:

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
self.ref.child("users").child(user.uid).setValue(["username": username])

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

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:

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
self.ref.child("users/\(user.uid)/username").setValue(username)

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

leer datos

Leer datos escuchando eventos de valor

Para leer datos en una ruta y escuchar cambios, use observeEventType:withBlock de FIRDatabaseReference para observar eventos FIRDataEventTypeValue .

Tipo de evento Uso típico
FIRDataEventTypeValue Lea y escuche los cambios en todo el contenido de una ruta.

Puede utilizar el evento FIRDataEventTypeValue para leer los datos en una ruta determinada, tal como existen 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 snapshot que contiene todos los datos en esa ubicación, incluidos los datos secundarios. Si no hay datos, la instantánea devolverá false cuando llame a exists() y nil cuando lea 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:

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

El oyente recibe un FIRDataSnapshot que contiene los datos en la ubicación especificada en la base de datos en el momento del evento en su propiedad value . Puede asignar los valores al tipo nativo apropiado, como NSDictionary . Si no existen datos en la ubicación, el value es nil .

Leer datos una vez

Leer una vez usando getData()

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 de eventos de valor descritas anteriormente para leer datos y recibir notificaciones sobre las actualizaciones de los datos desde el backend. Estas técnicas 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 getData() para obtener una instantánea de los datos de la base de datos. Si por alguna razón getData() 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 siguiente ejemplo demuestra cómo recuperar el nombre de usuario público de un usuario una sola vez desde la base de datos:

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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 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.

Leer datos una vez con un observador.

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 observeSingleEventOfType 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:

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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)
}

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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);
}];

Actualizar o eliminar datos

Actualizar campos específicos

Para escribir simultáneamente en elementos secundarios específicos de un nodo sin sobrescribir otros nodos secundarios, utilice el método updateChildValues .

Al llamar a updateChildValues , 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 querer crear una publicación y actualizarla simultáneamente a la fuente de actividad reciente y a la fuente de actividad del usuario que publica. Para hacer esto, la aplicación de blogs usa un código como este:

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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)

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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];

Este ejemplo utiliza childByAutoId para crear una publicación en el nodo que contiene publicaciones para todos los usuarios en /posts/$postid y simultáneamente recuperar 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 updateChildValues , 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 un bloque de finalización

Si desea saber cuándo se han confirmado sus datos, puede agregar un bloque de finalización. Tanto setValue como updateChildValues ​​toman un bloque de finalización opcional que se llama cuando la escritura se ha confirmado en la base de datos. Este oyente puede resultar útil para realizar un seguimiento de qué datos se han guardado y qué datos aún se están sincronizando. Si la llamada no tuvo éxito, se pasa al oyente un objeto de error que indica por qué se produjo el error.

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
do {
  try await ref.child("users").child(user.uid).setValue(["username": username])
  print("Data saved successfully!")
} catch {
  print("Data could not be saved: \(error).")
}

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
[[[_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.");
  }
}];

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 nil como valor para otra operación de escritura como setValue o updateChildValues . Puede utilizar esta técnica con updateChildValues ​​para eliminar varios elementos secundarios en una única llamada API.

Separar oyentes

Los observadores no dejan de sincronizar datos automáticamente cuando sales de un ViewController . Si un observador no se elimina correctamente, continúa sincronizando datos con la memoria local. Cuando ya no sea necesario un observador, elimínelo pasando el FIRDatabaseHandle asociado al método removeObserverWithHandle .

Cuando agrega un bloque de devolución de llamada a una referencia, se devuelve un FIRDatabaseHandle . Estos identificadores se pueden utilizar para eliminar el bloqueo de devolución de llamada.

Si se han agregado varios oyentes a una referencia de base de datos, se llama a cada oyente cuando se genera un evento. Para detener la sincronización de datos en esa ubicación, debe eliminar todos los observadores en una ubicación llamando al método removeAllObservers .

Llamar a removeObserverWithHandle o removeAllObservers en un oyente no elimina automáticamente los oyentes registrados en sus nodos secundarios; también debes realizar un seguimiento de esas referencias o identificadores para eliminarlos.

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.

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:

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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)
  }
}

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
[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);
  }
}];

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. El valor contenido en la clase FIRMutableData es inicialmente el último valor conocido del cliente para la ruta, o nil si no hay ninguno. El servidor compara el valor inicial con su valor actual y acepta la transacción si los valores coinciden, o la rechaza. 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.

Rápido

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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)

C objetivo

Nota: Este producto de Firebase no está disponible en el destino del clip de aplicación.
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 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 activo. Cuando se escriben datos, primero se escriben en esta versión local. 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 que se escriba cualquier dato en 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