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 demande une certaine réflexion préalable. Plus important encore, vous devez planifier la manière dont les données seront enregistrées et récupérées ultérieurement pour simplifier ce processus.

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 les fournir à l'aide de childByAutoId.

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

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 sous un chemin d'accès, tel que /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 pour la structure des données

Éviter d'imbriquer des 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 comprendre pourquoi les données imbriquées sont mauvaises, 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 dans la liste des salons en ne téléchargeant que quelques octets par conversation, en extrayant rapidement les métadonnées pour lister ou afficher les salons dans une UI. Les messages peuvent être récupérés séparément et affichés à mesure qu'ils arrivent, 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 à 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. Vous pouvez souvent dénormaliser les données à l'aide d'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 dans l'enregistrement d'Ada et sous 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