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 Firebase Realtime.
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 puis 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 Firebase Realtime 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 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 utilisateur ou des noms sémantiques, ou elles peuvent vous être fournies à l'aide push()
.
Si vous créez vos propres clés, elles doivent être codées en UTF-8, peuvent contenir un maximum de 768 octets et ne peuvent pas contenir de fichiers .
, $
, #
, [
, ]
, /
ou 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.
Par exemple, considérons 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 sur 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
Évitez d'imbriquer les données
Étant donné que la base de données Firebase Realtime 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 sur un nœud de votre base de données, vous lui accordez également l'accès à toutes les données de 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 un exemple de la raison pour laquelle les données imbriquées sont mauvaises, considérons la structure suivante à imbrication multiple :
{
// 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, parcourir 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 divisées en chemins séparés, également appelés dénormalisation, elles peuvent être téléchargées efficacement lors d'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 des 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, permettant à l'interface utilisateur de rester réactive et rapide.
Créez des données évolutives
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 de données, comme indiqué dans Tri et filtrage des données .
Mais même cela pourrait s’avérer insuffisant. Considérons, par exemple, une relation bidirectionnelle entre utilisateurs et 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 une manière élégante de répertorier les groupes auxquels un utilisateur appartient 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. Désormais, alovelace
est indexé dans un groupe et techpioneers
est répertorié dans le profil d'Ada. Donc pour supprimer Ada du groupe, il faut la mettre à jour à deux endroits.
Il s’agit d’une redondance nécessaire pour les relations bidirectionnelles. Il vous permet de récupérer rapidement et efficacement les adhésions d'Ada, même lorsque la liste d'utilisateurs ou de groupes se compte par 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 enregistrements.
Cette approche, en inversant les données en répertoriant les ID sous forme de 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 bien plus efficace que l'interrogation ou l'analyse des données.