Ce guide couvre certains des concepts clés de l'architecture des données et les meilleures pratiques pour structurer les données JSON dans votre base de données en temps réel Firebase.
Construire une base de données correctement structurée nécessite un peu de prévoyance. Plus important encore, vous devez planifier la manière dont les données seront enregistrées et récupérées ultérieurement pour rendre ce processus aussi simple que possible.
Comment les données sont structurées : c'est un arbre JSON
Toutes les données de la base de données en temps réel Firebase 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 pas de tables ou d'enregistrements. Lorsque vous ajoutez des données à l'arborescence JSON, elles deviennent un nœud dans la structure JSON existante avec une clé associée. Vous pouvez fournir vos propres clés, telles que des identifiants d'utilisateur ou des noms sémantiques, ou elles peuvent vous être fournies à l'aide de la méthode push()
.
Si vous créez vos propres clés, elles doivent être encodées en UTF-8, peuvent contenir un maximum de 768 octets et ne peuvent pas contenir de fichiers .
, $
, #
, [
, ]
, /
ou 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.
Par exemple, considérez une application de chat qui permet aux utilisateurs de stocker un profil de base et une liste de contacts. Un profil utilisateur typique se trouve dans un chemin, 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 comme certains types natifs qui correspondent aux types JSON disponibles pour vous aider à écrire un code plus maintenable.
Meilleures pratiques pour la structure des données
Éviter l'imbrication des données
Étant donné que la base de données en temps réel Firebase permet d'imbriquer des données jusqu'à 32 niveaux de profondeur, vous pourriez être tenté de penser que cela devrait être la structure par défaut. Cependant, 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 à quelqu'un 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 garder votre structure de données aussi plate que possible.
Pour un exemple de la raison pour laquelle les données imbriquées sont incorrectes, considérons 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 dans les données devient problématique. Par exemple, la liste des titres des conversations de chat nécessite que l'intégralité de l'arborescence chats
, y compris tous les membres et messages, soit téléchargée sur le client.
Aplatir les structures de données
Si les données sont plutôt divisées en chemins séparés, également appelés dénormalisation, elles peuvent être téléchargées efficacement dans des appels séparés, 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 de parcourir la liste des salles en téléchargeant seulement quelques octets par conversation, en récupérant rapidement les métadonnées pour répertorier ou afficher les salles dans une interface utilisateur. Les messages peuvent être récupérés séparément et affichés au fur et à mesure de leur arrivée, ce qui permet à l'interface utilisateur de rester réactive et rapide.
Créer des données qui évoluent
Lors de la création d'applications, il est souvent préférable de télécharger un sous-ensemble d'une liste. Ceci 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. Plusieurs fois, vous pouvez dénormaliser les données en utilisant une requête pour récupérer un sous-ensemble des données, comme indiqué dans Récupérer les données .
Mais même cela peut être insuffisant. Considérons, par exemple, une relation bidirectionnelle entre les utilisateurs et les groupes. Les utilisateurs peuvent appartenir à un groupe et les groupes comprennent une liste d'utilisateurs. Quand vient le temps de décider à quels groupes appartient un utilisateur, les choses se compliquent.
Ce qu'il faut, c'est un moyen élégant de répertorier les groupes auxquels appartient un utilisateur et de récupérer uniquement les données de ces groupes. Un index des 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. Maintenant, alovelace
est indexé sous un groupe et techpioneers
est répertorié dans le profil d'Ada. Donc, pour supprimer Ada du groupe, il doit être mis à jour à deux endroits.
Il s'agit d'une redondance nécessaire pour les relations à double sens. Il vous permet de récupérer rapidement et efficacement les adhésions d'Ada, même lorsque la liste des utilisateurs ou des groupes se chiffre en millions ou lorsque les règles de sécurité de la base de données en temps réel empêchent l'accès à certains des enregistrements.
Cette approche, qui inverse les données en répertoriant les ID en tant que clés et en définissant la valeur sur true, rend la vérification d'une clé aussi simple 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.