sexta-feira, 7 de março de 2014

Armazenando dicionários no MongoDB com MongoDict

Muitas das vezes que precisamos persistir dados chave-valor optamos por utilizar bases de dados focados somente em armazenar esse tipo de dado: as chamadas key-value stores, como por exemplo Riak, Redis e Memcached (esse último, persiste apenas em memória). Porém, adicionar mais um software/serviço em uma aplicação trás mais trabalho para a equipe (deployment, manutenção) e maior possibilidade de falhas (não somente de segurança, mas relacionadas a lentidão e downtime também).

Riak logo   Redis logo   Memcached logo

Caso você já tenha o MongoDB rodando em sua infraestrutura e necessite de uma key-value store, você poderá utilizar a biblioteca Python mongodict, que desenvolvi em julho de 2012 para aproveitar a infra já disponível e, ao mesmo tempo, poder utilizar os dados persistidos com facilidade.

Dicionários no MongoDB

Desenvolvedores Python são hash-addicted (rá!) - usamos muito os dicionários, para tudo (às vezes até excessivamente). Meu objetivo ao desenvolver o mongodict foi justamente aproveitar a interface que já conhecemos de dicionários (dict[chave] = valor) para persistir os dados no MongoDB sem precisar utilizar uma API diferente da que já estamos acostumados.

Logo MongoDB

Instalando

Sem mais blá blá blá, vamos lá: o mongodict está disponível no PyPI então, para instalá-lo, basta executar o comando:

pip install mongodict

Ele funciona em Python 2.7 e 3.3.

Usando

A biblioteca é bem simples: existe apenas uma classe MongoDict dentro do módulo. Aprenda com exemplos:

from mongodict import MongoDict

# Cria uma instância do "dicionário", já conectando no mongod
# Argumentos `host`, `port`, `database` e `collection` são opcionais
mydict = MongoDict()

mydict['answer'] = 42
print(mydict['answer']) # 42

print('answer' in mydict) # True
del mydict['answer']
print('answer' in mydict) # False

mydict.update({'spam': 'eggs', 'ham': 'damn'})
for key, value in mydict.items():
    print('{} = {}'.format(key, value))
# ham = damn
# spam = eggs

Para assegurar que a classe MongoDict segue o protocolo MutableMapping, utilizei os testes do próprio CPython! :-)

Serialização de Dados

O mongodict utiliza a biblioteca pickle para serializar/desserializar os dados (apenas os valores, não as chaves), portanto, qualquer dado que puder ser serializado com a pickle poderá ser salvo.

Porém, em alguns casos é desejável alterar o serializador. Digamos, por exemplo, que eu esteja salvando o conteúdo de arquivos HTML e queira compactá-los para economizar espaço em meu servidor - daí basta passar o parâmetro codec:

from zlib import compress, decompress
from urllib import urlopen

from mongodict import MongoDict

mydict = MongoDict(codec=(compress, decompress))
url = 'http://www.CursoDeArduino.com.br/'
mydict['curso-de-arduino'] = urlopen(url).read()
print(mydict['curso-de-arduino'])
# <... imprime o HTML da página ...>

Nesse caso, se conectarmos no MongoDB diretamente podemos ver o tamanho armazenado lá:

colecao = mydict._collection
documento = colecao.find_one({'_id': 'curso-de-arduino'})
print(len(documento['v'])) # 5763
print(len(mydict['curso-de-arduino'])) # 20076

Cuidado com Objetos Mutáveis

Caso o valor de uma das chaves seja um objeto mutável (por exemplo: uma lista), lembre-se que alterar o objeto recuperado do MongoDict não irá atualizá-lo no banco (pois ele fica em memória e é um objeto Python nativo). Por exemplo:

from mongodict import MongoDict

mydict = MongoDict()
mydict['compras'] = ['tomate', 'rúcula', 'queijo']
mydict['compras'].append('azeite')
print(mydict['compras'])
# ['tomate', 'r\xc3\xbacula', 'queijo']

Para corrigir isso, devemos explicitamente atualizar o valor da chave:

compras = mydict['compras']
compras.append('azeite')
mydict['compras'] = compras
print(mydict['compras'])
# ['tomate', 'r\xc3\xbacula', 'queijo', 'azeite']

Autenticação

Para autenticar-se no servidor MongoDB, utilize o parâmetro auth:

from mongodict import MongoDict

mydict = MongoDict(auth=('user', 'myprecious'))

Dica Bônus

Caso você ache chato ter que acessar mydict['chave'] e prefira mydict.chave, utilize a biblioteca attrdict. Instale-a a partir do PyPI:

pip install attrdict

E para usar, é bem fácil:

from attrdict import AttrDict
from mongodict import MongoDict

mydict = AttrDict(MongoDict())
mydict.answer = 42
mydict.question = '?'

print('Answer to "{question}" = {answer}'.format(**mydict))

Você só conseguirá acessar como atributo o primeiro nível, ou seja, mydict.chave.outra_chave não funcionará (utilize mydict.chave['outra_chave']).

Contribuindo

Caso queira contribuir, acesse o issue tracker do mongodict no GitHub. Fique à vontade para relatar bugs, sugerir funcionalidades e enviar pull requests! =)