Estructuración de datos con Firebase Realtime Database para C++

Estructuración de datos

Esta guía cubre algunos de los conceptos clave en la arquitectura de datos y las mejores prácticas para estructurar los datos JSON en su Firebase Realtime Database.

Crear una base de datos estructurada adecuadamente requiere bastante previsión. Lo más importante es que debe planificar cómo se guardarán y luego se recuperarán los datos para que el proceso sea lo más fácil posible.

Cómo se estructuran los datos: es un árbol JSON

Todos los datos de Firebase Realtime Database se almacenan como objetos JSON. Puede pensar en la base de datos como un árbol JSON alojado en la nube. A diferencia de una base de datos SQL, no hay tablas ni registros. Cuando agrega datos al árbol JSON, se convierte en un nodo en la estructura JSON existente con una clave asociada. Puede proporcionar sus propias claves, como ID de usuario o nombres semánticos, o se las pueden proporcionar mediante el método Push() .

Si crea sus propias claves, deben estar codificadas en UTF-8, pueden tener un máximo de 768 bytes y no pueden contener archivos . , $ , # , [ , ] , / o caracteres de control ASCII 0-31 o 127. Tampoco puede utilizar caracteres de control ASCII en los valores mismos.

Por ejemplo, considere una aplicación de chat que permite a los usuarios almacenar un perfil básico y una lista de contactos. Un perfil de usuario típico se encuentra en una ruta, como /users/$uid . El usuario alovelace podría tener una entrada en la base de datos similar a esta:

{
  "users": {
    "alovelace": {
      "name": "Ada Lovelace",
      "contacts": { "ghopper": true },
    },
    "ghopper": { ... },
    "eclarke": { ... }
  }
}

Aunque la base de datos utiliza un árbol JSON, los datos almacenados en la base de datos se pueden representar como ciertos tipos nativos que corresponden a los tipos JSON disponibles para ayudarle a escribir código más fácil de mantener.

Mejores prácticas para la estructura de datos.

Evite anidar datos

Debido a que Firebase Realtime Database permite anidar datos con una profundidad de hasta 32 niveles, es posible que tengas la tentación de pensar que esta debería ser la estructura predeterminada. Sin embargo, cuando recupera datos en una ubicación de su base de datos, también recupera todos sus nodos secundarios. Además, cuando le otorga a alguien acceso de lectura o escritura en un nodo de su base de datos, también le otorga acceso a todos los datos de ese nodo. Por lo tanto, en la práctica, es mejor mantener la estructura de datos lo más plana posible.

Para ver un ejemplo de por qué los datos anidados son malos, considere la siguiente estructura anidada múltiple:

{
  // This is a poorly nested data architecture, because iterating the children
  // of the "chats" node to get a list of conversation titles requires
  // potentially downloading hundreds of megabytes of messages
  "chats": {
    "one": {
      "title": "Historical Tech Pioneers",
      "messages": {
        "m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." },
        "m2": { ... },
        // a very long list of messages
      }
    },
    "two": { ... }
  }
}

Con este diseño anidado, la iteración de los datos se vuelve problemática. Por ejemplo, para enumerar los títulos de las conversaciones de chat es necesario descargar en el cliente todo el árbol chats , incluidos todos los miembros y los mensajes.

Aplanar estructuras de datos

Si, en cambio, los datos se dividen en rutas separadas, lo que también se denomina desnormalización, se pueden descargar de manera eficiente en llamadas separadas, según sea necesario. Considere esta estructura aplanada:

{
  // Chats contains only meta info about each conversation
  // stored under the chats's unique ID
  "chats": {
    "one": {
      "title": "Historical Tech Pioneers",
      "lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
      "timestamp": 1459361875666
    },
    "two": { ... },
    "three": { ... }
  },

  // Conversation members are easily accessible
  // and stored by chat conversation ID
  "members": {
    // we'll talk about indices like this below
    "one": {
      "ghopper": true,
      "alovelace": true,
      "eclarke": true
    },
    "two": { ... },
    "three": { ... }
  },

  // Messages are separate from data we may want to iterate quickly
  // but still easily paginated and queried, and organized by chat
  // conversation ID
  "messages": {
    "one": {
      "m1": {
        "name": "eclarke",
        "message": "The relay seems to be malfunctioning.",
        "timestamp": 1459361875337
      },
      "m2": { ... },
      "m3": { ... }
    },
    "two": { ... },
    "three": { ... }
  }
}

Ahora es posible recorrer la lista de salas descargando solo unos pocos bytes por conversación, obteniendo rápidamente metadatos para enumerar o mostrar salas en una interfaz de usuario. Los mensajes se pueden recuperar por separado y mostrarse a medida que llegan, lo que permite que la interfaz de usuario siga siendo rápida y receptiva.

Cree datos que se escalen

Al crear aplicaciones, suele ser mejor descargar un subconjunto de una lista. Esto es particularmente común si la lista contiene miles de registros. Cuando esta relación es estática y unidireccional, simplemente puede anidar los objetos secundarios debajo del principal.

En ocasiones, esta relación es más dinámica o puede ser necesario desnormalizar estos datos. Muchas veces puede desnormalizar los datos utilizando una consulta para recuperar un subconjunto de datos, como se explica en Recuperar datos .

Pero incluso esto puede resultar insuficiente. Consideremos, por ejemplo, una relación bidireccional entre usuarios y grupos. Los usuarios pueden pertenecer a un grupo y los grupos comprenden una lista de usuarios. Cuando llega el momento de decidir a qué grupos pertenece un usuario, la cosa se complica.

Lo que se necesita es una forma elegante de enumerar los grupos a los que pertenece un usuario y obtener solo datos de esos grupos. Un índice de grupos puede ser de gran ayuda aquí:

// An index to track Ada's memberships
{
  "users": {
    "alovelace": {
      "name": "Ada Lovelace",
      // Index Ada's groups in her profile
      "groups": {
         // the value here doesn't matter, just that the key exists
         "techpioneers": true,
         "womentechmakers": true
      }
    },
    ...
  },
  "groups": {
    "techpioneers": {
      "name": "Historical Tech Pioneers",
      "members": {
        "alovelace": true,
        "ghopper": true,
        "eclarke": true
      }
    },
    ...
  }
}

Es posible que observe que esto duplica algunos datos al almacenar la relación tanto en el registro de Ada como en el grupo. Ahora alovelace está indexado en un grupo y techpioneers aparece en el perfil de Ada. Entonces, para eliminar a Ada del grupo, debe actualizarse en dos lugares.

Ésta es una redundancia necesaria para las relaciones bidireccionales. Le permite recuperar de manera rápida y eficiente las membresías de Ada, incluso cuando la lista de usuarios o grupos asciende a millones o cuando las reglas de seguridad de Realtime Database impiden el acceso a algunos de los registros.

Este enfoque, invertir los datos al enumerar los ID como claves y establecer el valor en verdadero, hace que verificar una clave sea tan simple como leer /users/$uid/groups/$group_id y verificar si es null . El índice es más rápido y mucho más eficiente que consultar o escanear los datos.

Próximos pasos