Estruture seu banco de dados

Este guia aborda alguns dos principais conceitos de arquitetura de dados e práticas recomendadas para estruturar os dados JSON em seu Firebase Realtime Database.

Construir um banco de dados adequadamente estruturado requer um pouco de reflexão. Mais importante ainda, você precisa planejar como os dados serão salvos e posteriormente recuperados para tornar esse processo o mais fácil possível.

Como os dados são estruturados: é uma árvore JSON

Todos os dados do Firebase Realtime Database são armazenados como objetos JSON. Você pode pensar no banco de dados como uma árvore JSON hospedada na nuvem. Ao contrário de um banco de dados SQL, não existem tabelas ou registros. Quando você adiciona dados à árvore JSON, eles se tornam um nó na estrutura JSON existente com uma chave associada. Você pode fornecer suas próprias chaves, como IDs de usuário ou nomes semânticos, ou elas podem ser fornecidas usando o método push() .

Se você criar suas próprias chaves, elas deverão ser codificadas em UTF-8, podem ter no máximo 768 bytes e não podem conter arquivos . , $ , # , [ , ] , / ou caracteres de controle ASCII 0-31 ou 127. Você também não pode usar caracteres de controle ASCII nos próprios valores.

Por exemplo, considere um aplicativo de bate-papo que permite aos usuários armazenar um perfil básico e uma lista de contatos. Um perfil de usuário típico está localizado em um caminho, como /users/$uid . O usuário alovelace pode ter uma entrada de banco de dados parecida com esta:

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

Embora o banco de dados use uma árvore JSON, os dados armazenados no banco de dados podem ser representados como determinados tipos nativos que correspondem aos tipos JSON disponíveis para ajudá-lo a escrever um código mais sustentável.

Melhores práticas para estrutura de dados

Evite aninhar dados

Como o Firebase Realtime Database permite aninhar dados com até 32 níveis de profundidade, você pode ficar tentado a pensar que essa deveria ser a estrutura padrão. No entanto, ao buscar dados em um local do banco de dados, você também recupera todos os seus nós filhos. Além disso, ao conceder a alguém acesso de leitura ou gravação em um nó do seu banco de dados, você também concede acesso a todos os dados desse nó. Portanto, na prática, é melhor manter sua estrutura de dados o mais plana possível.

Para obter um exemplo de por que os dados aninhados são ruins, considere a seguinte estrutura aninhada múltipla:

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

Com esse design aninhado, a iteração pelos dados torna-se problemática. Por exemplo, listar os títulos das conversas de chat requer que toda a árvore chats , incluindo todos os membros e mensagens, seja baixada para o cliente.

Achatar estruturas de dados

Se, em vez disso, os dados forem divididos em caminhos separados, também chamado de desnormalização, eles poderão ser baixados com eficiência em chamadas separadas, conforme necessário. Considere esta estrutura achatada:

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

Agora é possível percorrer a lista de salas baixando apenas alguns bytes por conversa, buscando rapidamente metadados para listar ou exibir salas em uma UI. As mensagens podem ser buscadas separadamente e exibidas conforme chegam, permitindo que a IU permaneça ágil e rápida.

Crie dados escaláveis

Ao criar aplicativos, geralmente é melhor baixar um subconjunto de uma lista. Isto é particularmente comum se a lista contiver milhares de registros. Quando esse relacionamento é estático e unidirecional, você pode simplesmente aninhar os objetos filho no pai.

Às vezes, essa relação é mais dinâmica, ou pode ser necessário desnormalizar esses dados. Muitas vezes você pode desnormalizar os dados usando uma consulta para recuperar um subconjunto de dados, conforme discutido em Recuperar Dados .

Mas mesmo isso pode ser insuficiente. Considere, por exemplo, um relacionamento bidirecional entre usuários e grupos. Os usuários podem pertencer a um grupo e os grupos constituem uma lista de usuários. Quando chega a hora de decidir a quais grupos um usuário pertence, as coisas ficam complicadas.

O que é necessário é uma maneira elegante de listar os grupos aos quais um usuário pertence e buscar apenas dados desses grupos. Um índice de grupos pode ajudar bastante aqui:

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

Você pode notar que isso duplica alguns dados armazenando o relacionamento no registro de Ada e no grupo. Agora alovelace está indexado em um grupo e techpioneers está listado no perfil de Ada. Portanto, para excluir Ada do grupo, ela deve ser atualizada em dois lugares.

Esta é uma redundância necessária para relacionamentos bidirecionais. Ele permite que você busque associações de Ada de maneira rápida e eficiente, mesmo quando a lista de usuários ou grupos chega a milhões ou quando as regras de segurança do Realtime Database impedem o acesso a alguns registros.

Essa abordagem, invertendo os dados listando os IDs como chaves e definindo o valor como true, torna a verificação de uma chave tão simples quanto ler /users/$uid/groups/$group_id e verificar se ela é null . O índice é mais rápido e muito mais eficiente do que consultar ou verificar os dados.

Próximos passos