Padrões para modelagem de dados/documentos em MongoDB – Parte 3

Uma pergunta muito frequente que recebo é como modelar corretamente bases MongoDB. A resposta mais honesta é que depende.

Sua aplicação faz mais leitura que escritas? Que dados precisam estar agrupados quando é feito uma leitura? Quais são as preocupações com performance que devemos ter na sua aplicação? O quão grande serão os documentos? O quão grande será a base como um todo?

Todas essas perguntas, e muitas outras, definem como devemos projetar nossos schemas em MongoDB. Sim, MongoDB é schemaless, mas ainda assim, a maior parte dos problemas de performance (e frustrações dos desenvolvedores) que usam MongoDB advém de schemas ruins. Então, sim, você deve pensar sobre seu schema e essa série sobre os padrões mais comuns para schema MongoDB é justamente pra te ajudar com isso.

Nesta terceira e última parte da minha série sobre padrões para modelagem de dados para MongoDB, nós vamos estudar os últimos 4 padrões mais comuns de serem aplicados nas empresas que usam este banco. Para você que está chegando agora, os outros artigos desta série são:

E os padrões que já vimos até agora foram:

  • Approximation: para maior performance em escrita de estatísticas mas com menos precisão, adicionando uma camada de controle na aplicação;
  • Attribute: para maior performance em buscas de campos similares que estão sofrendo com múltiplos índices, tornando-os um atributo array;
  • Bucket: para reduzir o tamanho da base que cresce rapidamente com registros em real time, agregando-os em documentos;
  • Computed: para aumentar a performance de consulta de agregações de dados que podem ser pré-computados;
  • Document Versioning: para manter histórico de todas versões de um documento, sem prejudicar a performance de consulta;
  • Extended Reference: para diminuir os JOINs entre coleções mantendo cópias de dados essenciais e que não mudam com frequência nos documentos principais;
  • Outlier: para tratar exceções em documentos sem ferir o seu schema que atende a maioria da coleção e sem agredir a performance;
  • Pre-allocation: para ganho de tempo de escrita (em versões anteriores a MongoDB 3.2), pré-alocando o espaço para documentos com tamanho de campos previsível;

E na imagem abaixo você consegue ver a lista completa de padrões apresentados nesta série mais uma vez.

Padrões de Modelagem
Padrões de Modelagem

No artigo de hoje então, veremos os 4 do final da lista, que está em ordem alfabética, não tem nada a ver com popularidade do padrão ou importância.

Polymorphic

Este padrão, Polimórfico, é útil quando você tem uma variedade de documentos muito similares entre si (mas não idênticos) e precisam ser mantidos na mesma coleção pois isso facilita as consultas a essas informações e tem mais performance do que fazer JOINs. Ex: coleção de atletas de diferentes esportes, base de veículos de diferentes tipos, classificados online de diversos produtos, etc.

Vamos pegar este exemplo de coleção de atletas de diferentes esportes. Em um banco relacional, teríamos uma tabela-mãe contendo os campos em comum entre todos atletas e depois as tabelas-filhas, especializadas para cada esporte, com uma chave estrangeira para a tabela-mãe.

No entanto, em MongoDB, usando o padrão polimórfico, teríamos documentos com uma base semelhante e características exclusivas, ao mesmo tempo, como abaixo.

Padrão Polymorphic
Padrão Polymorphic

Assim, quando você desejar fazer consultas de todos atletas, consegue de maneira simples e performática. Se desejar fazer consultas de atletas de um esporte específico, considerando filtros específicos desse esporte, também consegue, de maneira simples e performática também.

Uma coisa interessante é que esse padrão aqui também funciona bem em sub-documentos, como abaixo, onde em alguns eventos são solo e outros em duplas, no tênis.

Polimorfismo em Sub-documentos
Polimorfismo em Sub-documentos

Na sua aplicação, obviamente, será necessário ter essa resiliência para os dados que virão do banco, até mesmo usando o polimorfismo presente em algumas linguagens de programação como Java e C#. Talvez possamos considerar isso como a “desvantagem” desse padrão, pois exige lógica adicional na aplicação.

Este é, de longe, o padrão mais usado em MongoDB, o que eu chamava simplesmente de “VIEW” antes de conhecer o nome certo, pois me lembrava as VIEWS dos bancos relacionais que justamente faziam esse papel de agregar em uma tabela só os dados de diferentes tabelas. Curiosamente, o caso de uso mais comum deste padrão é em aplicações Single View.

Mais informações em Polymorphic.

Schema Versioning

Este padrão, Versionamento de Schema, é útil quando o seu schema mudou ao longo da existência da sua base de dados e documentos com diferentes versões de schema precisam coexistir na mesma coleção. Em uma base relacional, mudar o schema é um processo relativamente delicado pois envolve uma análise profunda do schema atual vs novo, migração de dados e downtime da aplicação.

No MongoDB, é um processo igualmente importante, mas bem menos dolorido e sem downtime. Basicamente, se temos o documento abaixo, com uma pessoa e seus contatos telefônicos…

…e mais tarde decidimos adicionar um campo para celular, é bem simples, certo? Basta adicionar mais um campo…

Mas e se mais tarde entendemos que um telefone residencial (home) não é mais algo onipresente e ao mesmo tempo surgiram outras formas de comunicação ainda mais importantes, como as redes sociais.

Prevendo novas alterações, podemos repensar esta estrutura para um array de contatos, usando o padrão Attribute (que falei no primeiro post da série), muito mais flexível e igualmente poderoso. Neste caso, temos um grande ponto de ruptura e aqui vale a pena versionar o schema, colocando um campo para isso:

Ambas versões de schema podem coexistir tranquilamente e caberá adaptar na sua aplicação para verificar esse campo para conseguir utilizar os documentos corretamente. Essa seria a primeira desvantagem deste padrão.

A segunda desvantagem é que com o passar do tempo, você pode ter de criar um script de migração de schema, em lote no MongoDB, se começar a ter tantas versões de schema que adicione muita complexidade na sua aplicação. Eu mesmo não recomendaria conviver com mais do que 2 schemas e ainda assim, recomendo que a sua própria aplicação, conforme os cadastros forem sendo atualizados, eles já serem salvos no schema mais recente.

Mais informações em Schema Versioning.

Subset

Este padrão, Subconjunto, é útil quando a quantidade de memória RAM não está sendo o suficiente para o working set (os dados que são mais acessados, o Mongo mantém na memória) pois existem documentos grandes e, ao mesmo tempo, um subconjunto grande desses dados não são usados com frequência pela aplicação.

Como exemplo podemos citar documentos que possuem arrays de subdocumentos no seu interior. Muitas vezes não há a necessidade de se retornar todos estes elementos em uma consulta. Pense em um produto de um ecommerce com mil avaliações. Você vai listar todas elas na tela? Quando você faz uma consulta por um produto, necessariamente precisa-se retornar todas elas junto do documento?

Produto com reviews
Produto com reviews

Muito provavelmente que somente as 10 avaliações mais recentes façam sentido de serem exibidas logo de cara. Assim, ao invés de armazenar TODAS as reviews do produto junto do mesmo, podemos armazenar somente um subconjunto, como as 10 mais recentes, e as demais ficam em uma coleção específica de avaliações, como abaixo.

Padrão Subset
Padrão Subset

Este é apenas um exemplo, o mindset é de que no caso de documentos grandes que estejam onerando o working set, somente os dados mais acessados deveriam estar no documento principal e os demais em subdocumentos.

A desvantagem aqui é a gestão deste subconjunto. Afinal, neste exemplo, se você quer ter sempre as 10 reviews mais recentes do produto, toda vez que uma nova review for postada, você terá de adicioná-la no subconjunto e na coleção de reviews, além de remover a mais antiga do subconjunto, certo? E esta é uma regra simples, então tome cuidado com a gestão dos seus subconjuntos!

Mais informações em Subset.

Tree

Este padrão, Árvore, é útil quando você precisa armazenar dados hierárquicos no seu documento. Ex: gestores e subordinados de um funcionário, categorias e subcategorias de um produto, etc.

Enquanto que em um banco relacional isso geralmente é resolvido com diversos registros com chaves primárias e estrangeiras pra todo lado, em MongoDB a resolução é muito mais direta, embora envolva duplicação de dados.

Pegando o exemplo de categorias e subcategorias de um produto, o padrão Árvore define que o seu documento de produto deve ter um campo array para as categorias-pai (ancestor_categories, em ordem), além de um campo para a categoria imediatamente superior (parent_category), como abaixo.

Note que aqui usei apenas os nomes das categorias, mas que poderíamos ter subdocumentos com chave-valor, caso seja necessário. Note também, que ter um campo parent_category diretamente na raiz do documento permite usar os recursos de atravessamento em grafo do MongoDB.

No caso do use case de funcionários, bastaria ter um outro campo array para os subordinados dele, ao invés de apenas os seus gestores.

A vantagem deste padrão é a velocidade de carregar a árvore hierárquica deste documento e como desvantagem temos a duplicação de dados e a complexidade de atualização dos mesmos. Logo, é importante que ele seja aplicado em dados que mudem com muita frequência, embora a hierarquia em si possa mudar bastante, basicamente o mesmo risco do padrão Extended Reference, já citado anteriormente.

Mais informações em Tree.

E com isso encerro esta série de padrões de modelagem de dados em MongoDB. Espero que seja de utilidade prática para as suas aplicações e que tenha conseguido lhe mostrar um pouco mais da versatilidade e poder da orientação à documentos deste fantástico banco de dados.

Um abraço e até a próxima!

* Espero que este artigo tenha sido útil para você que está aprendendo MongoDB. Para conteúdo mais aprofundado, recomendo meus livros. Para videoaulas, recomendo o meu curso online.

Curso Node.js e MongoDB
Curso Node.js e MongoDB

Padrões para modelagem de dados/documentos em MongoDB – Parte 2

Que os bancos não relacionais revolucionaram a indústria de banco de dados, isso não podemos negar. Inicialmente com grande foco em Big Data e mais tarde como alternativas à abordagem relacional para resolver problemas específicos, como falo neste artigo.

No entanto, cada um deles possui todo um paradigma diferente de modelagem de seus dados, enquanto nos bancos relacionais de diferentes fabricantes temos o mesmo paradigma, o que acaba causando muita confusão, mesmo entre desenvolvedores veteranos.

Especificamente falando do MongoDB, o NoSQL mais popular do mercado, iniciei no artigo passado uma série sobre padrões de modelagem baseado em documentação oficial dos arquitetos do MongoDB, de onde vem também a imagem abaixo que resume os padrões definidos e suas aplicações na indústria. Padrões de Modelagem Os padrões que já exploramos até o momento foram:

  • Approximation: para maior performance em escrita de estatísticas mas com menos precisão, adicionando uma camada de controle na aplicação;
  • Attribute: para maior performance em buscas de campos similares que estão sofrendo com múltiplos índices, tornando-os um atributo array;
  • Bucket: para reduzir o tamanho da base que cresce rapidamente com registros em real time, agregando-os em documentos;
  • Computed: para aumentar a performance de consulta de agregações de dados que podem ser pré-computados;

Seguiremos vendo mais padrões hoje, na parte 2 desta série.

Document Versioning

Este padrão, Versionamento de Documento, é útil quando você precisa manter versões anteriores de um mesmo documento, para fins de histórico, mas o mais comum de ser acessado é o atual. Ex: contratos e leis que são alteradas ao longo do tempo mas que não podem ser simplesmente sobrescritas/atualizadas.

Para resolver este problema, cria-se uma coleção para fins de histórico do documento em questão e coloca-se um campo para versionamento do mesmo (um id, uma data, etc). Na coleção principal, fica sempre a versão mais recente do documento, para consulta rápida, e na coleção secundária, todas as versões, sendo esta uma coleção muito maior e consequentemente mais lenta para consulta.

Como desvantagens deste padrão temos o dobro de escritas (sempre uma vez na coleção corrente e outra na coleção arquivo) e também deve ajustar sua aplicação para sempre pegar o documento da coleção certa.

Mais informações em Document Versioning.

Extended Reference

Este padrão, Referência Extendida, é útil quando você teria de fazer diversos JOINs no modelo relacional para ter os dados que precisa que sejam exibidos juntos no sistema com grande frequência, juntando dados de diversas tabelas. Ex: catálogo de produtos, ordem de compra em um ecommerce, prontuário médico de paciente, etc.

Para resolver este problema, mantém-se a divisão em várias coleções, porém agregam-se os dados mais necessários de maneira duplicada no documento principal. Por exemplo, uma ordem de pedido, ao invés de ter apenas o id do cliente que fez a compra, pode ter também seu nome e seu telefone principal, bem como o nome, quantidade e preço de cada produto adquirido, que são os dados que serão exibidos no sistema na tela de resumo do pedido.

Extended Reference
Extended Reference

Na minha opinião, esse é um dos padrões que mais usei nos projetos que apliquei MongoDB como persistência e até conhecer esse nome eu chamava de Light Sub-documents, pois já entendia que usar versões menores (mais “light”) de documentos maiores funcionava muito bem nesse modelo de banco.

A desvantagem mais óbvia deste padrão é a duplicação dos dados entre os diferentes documentos, então o ideal é não fazer uma referência extendida de campos que possam mudar com frequência.

Mais informações em Extended Reference.

Outlier

Este padrão, Caso Isolado, é útil quando sua coleção está modelada de um jeito bacana, mas alguns documentos dela fogem à regras gerais que você criou, o que pode pôr abaixo toda a modelagem da maioria, por causa de um problema de poucos documentos. Ex: um usuário de rede social que tem os seus amigos em um array, mas alguém muito popular, tem tantos amigos, que não cabe em um array do MongoDB. Um documento de produto que possui fotos junto dos dados do produto, mas que alguns possuem tantas fotos que não cabem no mesmo documento.

Para resolver este problema, mantém-se na coleção os documentos com a modelagem que atende à maioria. No documento que foge à regra de modelagem, cria-se um campo que sinaliza que este documento possui dados adicionais, em outra coleção, como abaixo, de um documento de livro que contém tantos clientes que compraram ele, que precisa alocar os clientes adicionais em outra coleção (has_extras sinaliza isso).

A aplicação então, quando vai carregar os dados deste documento, ao encontrar a flag de que possui campos adicionais, os procura na outra coleção, usando o _id do documento original como filtro.

É verdade que o schema em MongoDB é dinâmico e que você pode pensar que não faz sentido este padrão se simplesmente podem haver campos diferentes entre os documentos. No entanto, existem limites do próprio MongoDB, de itens em um array, de KB em um documento e por aí vai.

Assim, ao invés de particionar toda sua coleção por causa de poucos documentos, você mantém os dados juntos e somente nos documentos que é necessário o particionamento, você o faz.

A desvantagem desta padrão é que a aplicação tem de manter este controle, de carregar dados adicionais quando encontrar a flag, adicionando complexidade adicional. Além disso, esses Casos Isolados terão uma performance inferior do que o normal da aplicação.

Mais informações em Outlier.

Pre-allocation

Este padrão, Pré-alocação, é útil quando você sabe a estrutura exata que seu documento vai ter e a sua aplicação apenas vai preenchê-lo com dados. Ex: documento de sala de cinema que já poderia ter os subdocumentos dos assentos pré-alocados.

Este padrão era mais útil nas versões anteriores à 3.2 do MongoDB, quando alocar espaço de maneira adiantada dava ganhos de performance no antigo sistema de arquivos MMAPv1. Com o advento do Wired Tiger e consequente descontinuação do MMAPv1, ele não é mais tão útil quanto antigamente.

A vantagem desta solução é a simplicidade da solução apenas, já que a performance não é mais significativa na alocação de espaço e como desvantagem temos o uso mais intenso de disco e memória por causa dos documentos maiores desde o início.

Sinceramente? Não vejo mais utilidade neste pattern, mas pode ser miopia da minha parte.

Para mais informações, leia Pre-allocation.

Na próxima parte desta série (clique aqui para ler), vou explicar os 4 últimos padrões de modelagem do MongoDB. Até lá!

* Espero que este artigo tenha sido útil para você que está aprendendo MongoDB. Para conteúdo mais aprofundado, recomendo meus livros. Para videoaulas, recomendo o meu curso online.

Curso Node.js e MongoDB
Curso Node.js e MongoDB

Padrões para modelagem de dados/documentos em MongoDB

Uma das coisas mais difíceis para quem está entrando agora neste mundo de bancos não-relacionais é modelagem de dados. E quando o assunto é MongoDB, o banco não relacional mais famoso do mercado segundo diversas pesquisas, não é diferente.

Muitos desenvolvedores começando com esta tecnologia “apanham” muito para se desvencilhar do paradigma relacional e abraçar a orientação à documentos, modelo do MongoDB. Muitos são os fatores para essa dificuldades, mas posso citar alguns que entendo que são os mais fortes: ausência de ensino formal nas instituições de ensino, baixa maturidade do modelo frente ao concorrente (o modelo relacional está no mercado há décadas) e adoção errada da tecnologia não relacional, sendo que este último eu já falei anteriormente aqui no blog.

O fato é, o uso e a modelagem de dados não relacionais varia muito mais do que no paradigma relacional e você não vai encontrar no mercado nada parecido com as 4 Formas Normais. Mas, se dermos um zoom em apenas uma das tecnologias, conseguimos colocar um pouco mais de luz neste desafio que é a modelagem de dados, neste caso, no MongoDB.

E este é o objetivo do artigo de hoje.

Construindo com Padrões

Assim como na engenharia de software nós temos os Padrões de Projeto (Design Patterns no original), o time do MongoDB resolveu compilar o conhecimento acumulado da última década de uso deste banco não relacional em alguns padrões de modelagem comuns e reutilizáveis, em uma excelente série de posts em seu blog, intitulada Building With Patterns.

Minha ideia é trazer em uma série de posts aqui no blog, os padrões descritos pelo time do MongoDB, mas não apenas traduzindo-os, mas tentando trazer de uma maneira mais didática cada um dos padrões. Começando pela matriz abaixo, achei ela sensacional. Ela é apenas uma linha guia, mas que facilita bastante para consultas rápidas. No eixo horizontal, os padrões de modelagem encontrados pelo time do MongoDB, e no vertical, os casos de uso mais comuns destes padrões.

Padrões de Modelagem
Padrões de Modelagem

Note que analisando os use cases listados pelo time do MongoDB, também podemos presumir onde eles entendem que ele melhor se adequa, o que corrobora com a minha máxima de alguns anos que MongoDB não substitui os bancos relacionais em todos cenários, mas sim em alguns cenários específicos, que, segundo o time do MongoDB, são:

  • Catalog: como catálogo de produtos e serviços;
  • Content Management: como em ECMs (gerenciadores de conteúdo corporativo);
  • Internet of Things: como em sistemas com sensores real-time, como na indústria 4.0;
  • Mobile: como em aplicações móveis de baixa latência e alta escala;
  • Personalization: como em sistemas com schema de dados personalizável para entregar conteúdo relevante aos usuários, muito usados no marketing digital;
  • Real-Time Analytics: como em sistemas de estatísticas real-time;
  • Single View: como em sistemas de dashboards, gerenciais e outros que agregam informações de diferentes fontes em uma base só;

Nesta série, vou passar rapidamente pela teoria de cada um dos padrões, explicando-os em alto nível, sua aplicação e linkando ao artigo original (em inglês) que o detalha mais profundamente. Se algum deles não ficar bem explicado, conto com o seu feedback nos comentários que procurarei trazer mais luz ao assunto.

Approximation

Este padrão, Aproximação, é útil quando temos que manter registro de estatísticas em grande volume na aplicação, estamos sofrendo com a quantidade de escritas e não precisamos que estas estatísticas sejam exatas, podem ser aproximadas. Ex: população de países, pageviews de um grande portal, número de resultados de pesquisa de um grande buscador, etc.

Para fazer isto, a aplicação deve armazenar as estatísticas recentemente recebidas em um cache por exemplo e somente escrever no banco de dados quando atingir um limiar aceitável (por ex, 100). Usando este limiar hipotético de 100:1, conseguimos reduzir drasticamente (em 99 vezes, neste caso) as escritas no MongoDB, liberando sua performance para operações mais importantes.

A desvantagem deste padrão é que as estatísticas não serão exatas e eventualmente pode haver pequena perda de dados caso ocorra falha no cache. Por isso é importante que seja usado somente em casos que possamos ter estatísticas aproximadas.

Mais informações em Approximation.

Attribute

Este padrão, Atributo, é útil quando temos diversos campos em um documento que são usados em filtros de consulta e estamos sofrendo com a quantidade de índices necessários para que as consultas performem com velocidade. Ex: as diferentes datas de lançamento de um filme internacional, as diferentes unidades de medida de um produto internacional, as diferentes traduções de um texto, etc.

Se um conjunto destes campos possuem características em comum, como o mesmo tipo, podemos agrupá-los em um atributo do tipo array, criando apenas um índice nele. Por exemplo, em uma base de filmes de cinema, ao invés de termos atributos de lançamento para cada país, criamos um atributo “lancamentos” como um array de subdocumentos estilo chave-valor, onde a chave seria o país e o valor seria a data de lançamento naquele país.

A contra-indicação deste padrão é se você não aplicá-lo no conjunto de atributos correto, que façam sentido serem agrupados em um atributo só.

Mais informações em Attribute.

Bucket

Este padrão, Balde, é útil quando temos um alto fluxo de dados de dados em tempo real e estamos sofrendo com a escrita elevada e o o crescimento rápido da base, o que acaba afetando as consultas. Ex: sensores que enviam dados em real time, monitoramento de recursos de servidores, etc.

Ao invés de escrever cada registro em um documento separado, criamos “baldes” de registros baseados em timestamp. Ou seja, se você recebe um registro por segundo, ao invés de termos 60 documentos por minuto, podemos ter um documento por minuto compreendendo o timestamp dos últimos 60 segundos.

Essa abordagem não reduz tanto a escrita quanto o padrão Approximation, pois estaremos escrevendo na mesma quantidade (mas não no mesmo volume de dados), mas mantém a fidelidade das estatísticas no detalhe.

A desvantagem é apenas a lógica de escrita que fica um pouco mais complexa, pois ao invés de simplesmente sair salvando novos documentos, você tem de guardar a estatística no “balde” certo.

Mais informações em Bucket.

Computed

Este padrão, Computado, é útil quando temos a necessidade e ter dados agregados em bases com grande volume, fazendo com que a performance de agregações não seja boa. Ex: soma de vendas por produto, visitantes de um grande portal por país, ocorrência de palavras em volumes de texto ou no exemplo abaixo, ingressos de cinema vendidos e receita por filme.

Para resolver este problema, deixamos os dados pré-computados para consulta posterior, com duas alternativas diferentes.

Alternativa 1, se a escrita não é frequente, podemos ter campos computados no documento principal que, a cada atualização, são recalculados. Por exemplo, um campo num_viewers em um documento de filme de cinema que é incrementado a cada nova venda de ingresso daquele filme.

Alternativa 2, se a escrita é frequente, a aplicação pode controlar um intervalo de tempo em que ela vai realizar as agregações e deixar salvo, provavelmente em uma base específica para armazenar estas computações que serão analisadas mais tarde.

Este é um padrão que adiciona relativa complexidade na aplicação, então não deve ser usado exceto quando realmente necessário.

Mais informações em Computed.

Para acompanhar os demais padrões, leia a parte 2 desta série.

* Espero que este artigo tenha sido útil para você que está aprendendo MongoDB. Para conteúdo mais aprofundado, recomendo meus livros. Para videoaulas, recomendo o meu curso online.

Curso Node.js e MongoDB
Curso Node.js e MongoDB