Structurer des données avec Firebase Realtime Database pour C++

Structurer les données

Ce guide couvre certains des concepts clés de l'architecture de données et les bonnes pratiques pour structurer les données JSON dans votre Firebase Realtime Database.

La création d'une base de données correctement structurée nécessite une certaine anticipation. Plus important encore, vous devez planifier la manière dont les données seront enregistrées et récupérées ultérieurement pour faciliter au maximum ce processus.

Structure des données : une arborescence JSON

Toutes les données Firebase Realtime Database sont stockées sous forme d'objets JSON. Vous pouvez considérer la base de données comme une arborescence JSON hébergée dans le cloud. Contrairement à une base de données SQL, il n'y a ni tables, ni enregistrements. Lorsque vous ajoutez des données à l'arborescence JSON, elles prennent la forme d'un nouveau nœud et d'une clé associée dans la structure JSON existante. Vous pouvez fournir vos propres clés, par exemple des identifiants utilisateur ou des noms sémantiques, ou elles peuvent être fournies à l'aide de la méthode Push().

Prenons l'exemple d'une application de chat qui permet aux utilisateurs de stocker un profil de base et une liste de contacts. Un profil utilisateur type se trouve dans un chemin d'accès tel que /users/$uid. L'utilisateur alovelace peut avoir une entrée de base de données qui ressemble à ceci :

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

Bien que la base de données utilise une arborescence JSON, les données stockées dans la base de données peuvent être représentées sous certains types natifs qui correspondent aux types JSON disponibles pour vous aider à écrire du code plus facile à gérer.

Bonnes pratiques pour la structure des données

Éviter d'imbriquer les données

Étant donné que la Firebase Realtime Database permet d'imbriquer des données jusqu'à 32 niveaux de profondeur, vous pourriez être tenté de penser qu'il s'agit de la structure par défaut. Toutefois, lorsque vous récupérez des données à un emplacement de votre base de données, vous récupérez également tous ses nœuds enfants. De plus, lorsque vous accordez à une personne un accès en lecture ou en écriture à un nœud de votre base de données, vous lui accordez également l'accès à toutes les données sous ce nœud. Par conséquent, en pratique, il est préférable de conserver une structure de données aussi plate que possible.

Pour comprendre pourquoi les données imbriquées sont une mauvaise pratique, examinez la structure à imbrication multiple suivante :

{
  // 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": { "..." }
  }
}

Avec cette conception imbriquée, l'itération dans les données devient problématique. Par exemple, pour lister les titres des conversations de chat, il est nécessaire de télécharger l'arborescence chats entière, y compris tous les membres et messages, sur le client.

Aplatir les structures de données

Si les données sont divisées en chemins distincts, également appelés dénormalisation, elles peuvent être téléchargées efficacement dans des appels distincts, selon les besoins. Considérez cette structure aplatie :

{
  // 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": { "..." }
  }
}

Il est désormais possible d'itérer dans la liste des salons en ne téléchargeant que quelques octets par conversation, en récupérant rapidement les métadonnées pour lister ou afficher les salons dans une interface utilisateur. Les messages peuvent être récupérés séparément et affichés à leur arrivée, ce qui permet à l'interface utilisateur de rester réactive et rapide.

Créer des données évolutives

Lorsque vous créez des applications, il est souvent préférable de télécharger un sous-ensemble d'une liste. Cela est particulièrement courant si la liste contient des milliers d'enregistrements. Lorsque cette relation est statique et unidirectionnelle, vous pouvez simplement imbriquer les objets enfants sous le parent.

Parfois, cette relation est plus dynamique ou il peut être nécessaire de dénormaliser ces données. Dans de nombreux cas, vous pouvez dénormaliser les données à l'aide d'une requête pour récupérer un sous-ensemble des données, comme indiqué dans Récupérer des données.

Mais même cela peut être insuffisant. Prenons l'exemple d'une relation bidirectionnelle entre les utilisateurs et les groupes. Les utilisateurs peuvent appartenir à un groupe, et les groupes comprennent une liste d'utilisateurs. Lorsque vient le moment de décider à quels groupes un utilisateur appartient, les choses se compliquent.

Il est nécessaire de disposer d'un moyen élégant de lister les groupes auxquels un utilisateur appartient et de ne récupérer que les données de ces groupes. Un index de groupes peut être très utile ici :

// 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
      }
    },
    // ...
  }
}

Vous remarquerez peut-être que cela duplique certaines données en stockant la relation à la fois sous l'enregistrement d'Ada et sous le groupe. Désormais, alovelace est indexé sous un groupe, et techpioneers est listé dans le profil d'Ada. Par conséquent, pour supprimer Ada du groupe, il doit être mis à jour à deux endroits.

Il s'agit d'une redondance nécessaire pour les relations bidirectionnelles. Elle vous permet de récupérer rapidement et efficacement les adhésions d'Ada, même lorsque la liste d'utilisateurs ou de groupes atteint des millions d'éléments ou lorsque les règles de sécurité Realtime Database empêchent l'accès à certains enregistrements.

Cette approche, qui consiste à inverser les données en listant les ID comme clés et en définissant la valeur sur "true", permet de vérifier une clé aussi simplement que de lire /users/$uid/groups/$group_id et de vérifier si elle est null. L'index est plus rapide et beaucoup plus efficace que l'interrogation ou l'analyse des données.

Étapes suivantes