Estruturar seu banco de dados

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

Criar corretamente um banco de dados estruturado exige planejamento. O mais importante é planejar como os dados serão salvos e depois 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. Pense no banco de dados como uma árvore JSON hospedada na nuvem. Ao contrário de um banco de dados SQL, não há tabelas nem registros. Quando você adiciona dados à árvore JSON, eles se tornam um nó na estrutura JSON com uma chave associada. Você pode fornecer suas próprias chaves, como IDs do usuário ou nomes semânticos, ou consegui-las usando push().

Caso queira criar as próprias chaves, codifique-as em UTF-8. Elas podem ter até 768 bytes e não podem conter os caracteres ., $, #, [, ], / nem os caracteres de controle ASCII de 0 a 31 ou 127. Não é possível usar caracteres de controle ASCII nos próprios valores.

Por exemplo, pense em um aplicativo de bate-papo em que os usuários armazenam 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": { ... }
  }
}

O banco de dados usa uma árvore JSON, mas os dados armazenados nele podem ser representados como tipos nativos que correspondam a tipos JSON disponíveis. Assim, você escreve códigos mais fáceis de atualizar.

Práticas recomendadas para a estruturação dos dados

Evitar o aninhamento de dados

Como o Firebase Realtime Database permite aninhar dados com até 32 níveis de profundidade, talvez você acredite que essa deva ser a estrutura padrão. Mas, quando você busca dados em um local do banco de dados, também recupera todos os nós filhos. Além disso, quando concede a um usuário o acesso de leitura ou gravação a um nó do banco de dados, ele também pode acessar todos os dados desse nó. Portanto, na prática, é melhor manter a estrutura de dados o mais simples possível.

Confira por que não é recomendável aninhar dados. Considere esta estrutura de vários aninhamentos:

{
  // 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 projeto aninhado, a iteração dos dados se torna um problema. Por exemplo, para listar os títulos de conversas de bate-papo, é necessário fazer o download de toda a árvore chats para o cliente, inclusive os membros e as mensagens.

Simplificar estruturas de dados

Se, em vez disso, os dados forem divididos em caminhos separados, como um processo de desnormalização, o download poderá ser feito em chamadas separadas, conforme a necessidade. Veja esta estrutura simplificada:

{
  // 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 fazendo o download de apenas alguns bytes por conversa, recuperando rapidamente metadados para listagem ou exibindo salas em uma IU. As mensagens podem ser carregadas separadamente e exibidas à medida que chegam, deixando a IU responsiva e rápida.

Criar dados escalonáveis

Para criar apps, é recomendado fazer o download de um subconjunto de uma lista. Isso é muito comum se a lista tiver milhares de registros. Quando essa relação é estática e unidirecional, basta agrupar os objetos filhos subordinados ao pai.

Às vezes, essa relação é mais dinâmica e convém desnormalizar esses dados usando uma consulta para recuperar um subconjunto de dados, conforme discutido em Recuperar dados.

Entretanto, até isso pode ser insuficiente. Considere, por exemplo, uma relação bidirecional entre usuários e grupos. Os usuários podem pertencer a um grupo, e os grupos incluem uma lista de usuários. A dificuldade aparece na hora de decidir a quais grupos um usuário pertence.

É necessário encontrar um meio eficiente de listar os grupos a que um usuário pertence e buscar dados somente desses grupos. Um índice dos grupos pode ser muito útil neste caso:

// 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ê perceberá que alguns dados são duplicados quando a relação é armazenada tanto no registro de Ada como no grupo. Agora, alovelace é indexado em um grupo e techpioneers é listado no perfil de Ada. Então, para excluir Ada do grupo, é necessário que isso seja atualizado nos dois locais.

Essa é uma redundância necessária nas relações bidirecionais. Ela permite que as associações de Ada sejam carregadas de maneira rápida e eficiente, mesmo quando a lista de usuários ou grupos atinge milhões ou quando as regras de segurança do Realtime Database impedem o acesso a alguns dos registros.

Essa abordagem, ao inverter os dados listando os códigos como chaves e definir o valor como true, torna a verificação de uma chave uma tarefa tão simples quanto ler /users/$uid/groups/$group_id e verificar se é null. A indexação é mais rápida e muito mais eficiente do que o envio de consultas ou a varredura dos dados.

Próximas etapas