Structurer votre base de données

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

Créer une base de données correctement structurée nécessite une bonne dose de réflexion. Plus important encore, vous devez planifier la manière dont les données seront enregistrées, puis récupérées ultérieurement pour simplifier ce processus au maximum.

Structure des données : il s'agit d'une arborescence JSON

Toutes les données Firebase Realtime Database sont stockées en tant qu'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, telles que des ID utilisateur ou des noms sémantiques, ou elles peuvent vous être fournies à l'aide de push().

Si vous créez vos propres clés, elles doivent être encodées en UTF-8, ne pas dépasser 768 octets et ne pas contenir les caractères de contrôle ASCII 0 à 31 ou 127, ni ., $, #, [, ], /. Vous ne pouvez pas non plus utiliser de caractères de contrôle ASCII dans les valeurs.

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 à un chemin, par exemple /users/$uid. L'utilisateur alovelace peut avoir une entrée de base de données qui se présente comme suit :

{
  "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 comme certains types natifs correspondant aux types JSON disponibles pour vous aider à écrire du code plus facile à gérer.

Bonnes pratiques concernant la structure des données

Éviter l'imbrication de données

Étant donné que Firebase Realtime Database permet d'imbriquer des données jusqu'à 32 niveaux, 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 à un utilisateur 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 votre structure de données aussi plate que possible.

Pour illustrer les raisons pour lesquelles les données imbriquées sont inadaptées, considérez la structure multi-imbriquée 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 des données devient problématique. Par exemple, pour lister les titres des conversations de chat, l'intégralité de l'arborescence chats, y compris tous les membres et les messages, doit être téléchargée sur le client.

Aplatir les structures de données

Si les données sont plutôt 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érons 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 la liste des salons en ne téléchargeant que quelques octets par conversation, ce qui permet de récupérer rapidement des métadonnées pour lister ou afficher les salles dans une interface utilisateur. Les messages peuvent être récupérés séparément et affichés à mesure qu'ils arrivent, ce qui permet à l'UI 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 à sens unique, vous pouvez simplement imbriquer les objets enfants sous l'objet parent.

Parfois, cette relation est plus dynamique, ou il peut être nécessaire de dénormaliser ces données. Souvent, vous pouvez dénormaliser les données en utilisant une requête pour récupérer un sous-ensemble de données, comme indiqué dans la section 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éterminer à quels groupes un utilisateur appartient, les choses se compliquent.

Il vous faut un moyen élégant de lister les groupes auxquels un utilisateur appartient et de récupérer uniquement les données de ces groupes. Un index de groupes peut vous aider à y parvenir :

// 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 dans l'enregistrement d'Ada et dans le groupe. alovelace est désormais indexé dans un groupe, et techpioneers est listé dans le profil d'Ada. Pour supprimer Ada du groupe, vous devez la mettre à jour à deux endroits.

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

Cette approche, qui inverse les données en listant les ID en tant que 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 s'il s'agit de null. L'index est plus rapide et beaucoup plus efficace que d'interroger ou d'analyser les données.

Étapes suivantes