sexta-feira, 13 de fevereiro de 2015

Campos Dinâmicos no MongoDB

Tudo bem pessoal?

Estou aqui para dar mais uma pequena dica sobre MongoDB. Vou falar hoje de um problema com campos dinâmicos que passei em meu projeto atual e como consegui resolve-lo com um ajuste no schema de dados.

No projeto em questão precisamos permitir que os usuários criem campos que não estão no schema padrão para armazenar seus conteúdos específicos. Esta necessidade de dinamismo no schema foi um dos motivos, dentre outros, que nos levou a escolher o MongoDB.

O problema


O MongoDB possui schema flexível, o que quer dizer que o banco de dados não vai te forçar a respeitar regras de modelagem de dados, ficando a cargo da aplicação lidar com isso. Em outras palavras, é possível, por exemplo, criar campos novos a qualquer momento sem a necessidade de realizar migrações.

Vamos a um exemplo. Imagine que criamos uma aplicação de cadastro de pessoas onde prevemos os campos nome, telefone e email. Um registro dessa aplicação seria mais ou menos assim:

> db.people.findOne()
{
        "_id" : ObjectId("54dcec8d603258ab0c69832d"),
        "name" : "Rafael",
        "email" : "rafael@test.com",
        "fone" : "99 9999 9999"
}

Para melhorar a busca por pessoas poderíamos indexar os campos nome e email, por exemplo.

Imagine agora que um dos usuários da aplicação precisa armazenar a cidade de cada pessoa cadastrada, mas que essa é uma necessidade apenas deste usuário e que não convêm ajustar o schema padrão para isso.

Não vou entrar agora nos detalhes de como a aplicação se adapta a necessidade do usuário e permite ao mesmo adicionar aos seus registros novos campos, este é um assunto que merece um post só para ele.

Um exemplo deste registro ajustado poderia ser assim:

> db.people.findOne({name:'José'})
{
        "_id" : ObjectId("54dceffb603258ab0c698330"),
        "name" : "José",
        "email" : "jose@test.com",
        "fone" : "99 9999 9999",
        "custom_city" : "São Paulo"

}

O campo "custom_city" foi adicionado ao registro que esta na mesma collection do anterior. O prefixo "custom" foi usado para mostrar que este é um campo customizado e que não faz parde do schema padrão.

Até aqui tudo bem e parece não haver problemas, pois os usuários da nossa aplicação imaginária conseguem criar seus campos customizados e salvar conteúdo neles. Mas e se um usuário precisar buscar registros através de um campo customizado? E se esta busca ficar muito lenta?

Aqui esbarramos em um problema, pois não sabemos quais campos customizados serão criados pelos usuários e não temos como indexa-los como os demais. Poderíamos indexa-los posteriormente mas logo perceberíamos que é inviável pois demandaria muito trabalho e a quantidade de índices no banco cresceria demais. Sendo assim, precisamos ajustar a modelagem dos dados para permitir que os campos customizados sejam automaticamente indexados pelo MongoDB.

A solução


O MongoDB nos permite armazenar estruturas de dados complexas, com um grande nível de aninhamento e permite também indexar e recuperar registros através destes campos aninhados. Sendo assim, podemos ajudar a modelagem de dados para armazenar estes campos customizados em um estrutura chave-valor, o que nos da toda flexibilidade necessária sem perder performance!

O registro apresentado anteriormente ficaria dessa forma nesta nova modelagem:

> db.people.findOne({name:'José'})
{
        "_id" : ObjectId("54dceffb603258ab0c698330"),
        "name" : "José",
        "email" : "jose@test.com",
        "fone" : "99 9999 9999",
        "customs" : [
                {
                        "k" : "city",
                        "v" : "São Paulo"
                }
        ]
}

O campo "custom_city" foi removido e em seu lugar existe um campo "custom" que é uma lista (ou array) de estruturas chave-valor onde o nome do campo é guardado na chave "k" e seu valor na chave "v".

Desta forma é possível indexar todos os campos customizados que venham a ser criados com um único índice, da seguinte forma:

> db.people.ensureIndex({'customs.k': 1, 'customs.v': 1})

Agora, para buscar pessoas que moram em São Paulo, por exemplo, fica assim:

> db.people.findOne({'customs.k': 'city', 'customs.v': 'São Paulo'})
{
        "_id" : ObjectId("54dceffb603258ab0c698330"),
        "name" : "José",
        "email" : "jose@test.com",
        "fone" : "99 9999 9999",
        "customs" : [
                {
                        "k" : "city",
                        "v" : "São Paulo"
                }
        ]
}

Com esta modelagem é possível fazer buscas bem complexas, usando operadores como $elemMatch, $all e outros!

E para mostrar que o índice que criamos esta sendo usado basta usar o explain:

> db.people.find({'customs.k': 'city', 'customs.v': 'São Paulo'}).explain()
{
        "cursor" : "BtreeCursor customs.k_1_customs.v_1",
        "isMultiKey" : false,
        "n" : 1,
        "nscannedObjects" : 1,
        "nscanned" : 1,
        "nscannedObjectsAllPlans" : 1,
        "nscannedAllPlans" : 1,
        "scanAndOrder" : false,
        "indexOnly" : false,
        "nYields" : 0,
        "nChunkSkips" : 0,
        "millis" : 0,
        "indexBounds" : {
                "customs.k" : [
                        [
                                "city",
                                "city"
                        ]
                ],
                "customs.v" : [
                        [
                                "São Paulo",
                                "São Paulo"
                        ]
                ]
        },
        "server" : "note:27017",
        "filterSet" : false

}

Conclusão

O MongoDB nos garante muita flexibilidade e isso é ótimo, mas quanto mais opções existem, maior é a chance de escolher errado. Por isso é muito importante planejar com cuidado a modelagem de dados para garantir que o banco continuará respondendo bem conforme a massa de dados cresce!

Com essa solução consegui resolver meu problema e tornei viável a funcionalidade de campos dinâmicos nesse sistema, graças ao MongoDB, mas ele não é o único com tais capacidades. Existem outros bancos noSQL com características muito parecidas como o CouchDB e até bancos relacionais como o PostgreSQL com o hstore que eu gostaria muito de testar mais ainda não consegui.

Bom pessoal, espero que o post possa ajudar!

Criticas e sugestões são sempre bem vindos!


Referencias

http://askasya.com/post/dynamicattributes
http://docs.mongodb.org/manual/core/index-multikey/