Skip to content

Começando com Python e MongoDB

by ivan on fevereiro 5th, 2012

O que é MongoDB?

De MongoDB.org:

MongoDB (from “humongous”) is a scalable, high-performance, open source NoSQL database. (MongoDB é um banco de dados NoSQL escalável, de alto desempenho e open source).

MongoDB é orientado a documentos

MongoDB é um banco de dados orientado a documentos, ao invés de armazenar dados em linhas e colunas como em um banco de dados relacional, você armazena documentos em coleções. Os documentos são basicamente objetos JSON (tecnicamente BSON).

MongoDB tem uma linguagem de consulta flexível

Essa é uma das características que torna prazeroso trabalhar com MongoDB, principalmente se você vem de um outro banco de dados NoSQL onde as consultas têm algum tipo de restrição (armazenamentos chave-valor que podem somente ser consultados pela chave, por exemplo). Segue um exemplo de uma consulta que retorna todas as postagens de um projeto em um sistema que contém vários projetos:

blog_post.find({'state':'published',
                'app_config_id':{'$in':app_config_ids}})

Há também diversos outros operadores como ‘$lt’, ‘$nin’, ‘$not’ e ‘$or’, que permitem que você crie consultas mais complexas.

MongoDB é rápido e escalável

Um simples nó do MongoDB é capaz de servir milhares de requisições por segundo com um hardware dos mais baratos. Quando for necessário escalar além disso, pode-se usar replicação (deixando diversas cópias dos dados em servidores diferentes) ou sharding (particionar os dados entre os servidores). MongoDB ainda contém a lógica para carregar e balancear automaticamente tais partes à medida em que o banco cresce.

Começando com MongoDB

Primeiro é necessário instalar os pacotes mongodb e mongodb-server. Em seguida basta iniciar o deamon do MongoDB:

$ mongod --dbpath=/home/user/local/mongodb --bind_ip 0.0.0.0;127.0.0.1

Os parâmetros são opcionais, mas sempre prefiro utilizar uma pasta local (por não necessitar da senha de root ou do sudo) e fornecer uma lista de IPs de onde o servidor irá escutar. É interessante colocar esse comando em seu .bashrc.

Vamos criar um environment usando o virtualenv:

$ virtualenv mongo -p /usr/bin/python2.6

Vamos trabalhar diretamente com o PyMongo, embora seja interessante utilizar a linha de comando ‘mongo’ do MongoDB. Você pode fazer isso acessando o site do MongoDB no botão try it out. Ativando o ambiente e instalando o pymongo:

$ source mongo/bin/activate
$ pip install pymongo

Criando uma conexão

>>> from pymongo import Connection
>>> connection = Connection()

O código acima realiza uma conexão no host e porta padrão. É possível especificá-los:

>>> connection = Connection('127.0.0.1', 27017)

Obtendo um banco

Uma única instância do MongoDB suporta múltiplos e independentes bancos de dados. Usando PyMongo você pode acessar um banco de dados acessando um atributo de uma instância de Connection:

>>> db = connection.test_database

Se o nome de seu banco tiver caracteres que não são permitidos para nomes de variáveis, você pode acessá-lo na forma de dicionário:

>>> db = connection['test-database']

Obtendo uma coleção

Uma coleção (ou collection) é um grupo de documentos armazenados no MongoDB e pode ser comparada de modo grosseiro a uma tabela em um banco de dados relacional. Obter uma coleção no PyMongo funciona da mesma forma que obter um banco de dados:

>>> collection = db.test_collection

ou:

>>> collection = db['test-collection']

Uma nota importante sobre collections e banco de dados no MongoDB é que não é realizada nenhuma operação até que o primeiro documento seja inserido, ou seja, neste momento nossa collection e nossa database ainda não foram persistidas.

Documentos

Os dados no MongoDB são representados (e armazenados) usando documentos JSON-like. No PyMongo usamos dicionários para representar documentos. O seguinte dicionário pode ser usado para representar um post de blog:

>>> import datetime
>>> post = {"author": "Mike",
...         "text": "My first blog post!",
...         "tags": ["mongodb", "python", "pymongo"],
...         "date": datetime.datetime.utcnow()}

Os documentos podem conter tipos nativos do Python (como instâncias de datetime.datetime) que serão convertidos automaticamente em tipos do BSON.

Inserindo um documento

Para inserir um documento em uma coleção basta utilizar o método insert():

>>> posts = db.posts
>>> posts.insert(post)
ObjectId('...')

Quando um documento é inserido, a chave especial "_id" é automaticamente adicionada a ele se o mesmo não a tiver. "_id" deve ser único dentro da coleção. insert() retorna o valor do "_id" do documento inserido. Após inserir o primeiro documento, a coleção posts é criada no servidor. Podemos verificar isso listando todas as coleções do banco:

>>> db.collection_names()
[u'posts', u'system.indexes']

Obtendo um único documento com find_one()

O tipo mais básico de consulta no MongoDB é find_one(). Ela retorna um único documento correspondente à consulta (ou None):

>>> posts.find_one()
{u'date': datetime.datetime(2012, 2, 4, 23, 6, 3, 554000), u'text': u'My first blog post!',
    u'_id': ObjectId('4f2dbb9c856f1d27bd000000'), u'author': u'Mike',
    u'tags': [u'mongodb', u'python', u'pymongo']}

O resultado é um dicionário correspondente à consulta realizada. É possível também realizar uma filtragem:

>>> posts.find_one({'author': 'Mike'})
{u'date': datetime.datetime(2012, 2, 4, 23, 6, 3, 554000), u'text': u'My first blog post!',
    u'_id': ObjectId('4f2dbb9c856f1d27bd000000'), u'author': u'Mike',
    u'tags': [u'mongodb', u'python', u'pymongo']}
>>> posts.find_one({'author': 'Elliot'})
>>>

Vale ressaltar que find_one() deve ser usado quando você está interessado somente no primeiro resultado correspondente à consulta. É importante lembrar também que o MongoDB armazena os dados no formato BSON. Strings BSON são codificadas em UTF-8 então o PyMongo precisa ter certeza de que todas as strings inseridas contém apenas dados válidos em UTF-8. Strings regulares (<type ‘str’>) são validadas e armazenadas sem alterações. Strings unicode (<type ‘unicode’>) são antes codificadas para UTF-8.

Inserções em massa

É possível fazer insersões em massa passando um iterable como primeiro argumento do insert(), que salvará os dados através de um único comando:

>>> new_posts = [{"author": "Mike",
...               "text": "Another post!",
...               "tags": ["bulk", "insert"],
...               "date": datetime.datetime(2009, 11, 12, 11, 14)},
...              {"author": "Eliot",
...               "title": "MongoDB is fun",
...               "text": "and pretty easy too!",
...               "date": datetime.datetime(2009, 11, 10, 10, 45)}]
>>> posts.insert(new_posts)
[ObjectId('...'), ObjectId('...')]

Repare que a chamada a insert() agora retorna uma lista de instâncias de ObjectId. Repare também que new_posts[1] não contém “tags” e contém “title”, o que nos permite dizer que MongoDB é schema-free.

Consultando mais de um documento

Para obter mais do que um único documento como resultado da consulta devemos utilizar o método find(), que retorna uma instância de Cursor, permitindo a iteração sobre os documentos correspondentes. Podemos, por exemplo, iterar sobre cada documento da coleção posts:

>>> for post in posts.find():
...     post
...
{u'date': datetime.datetime(2012, 2, 4, 23, 6, 3, 554000), u'text': u'My first blog post!',
    u'_id': ObjectId('4f2dbb9c856f1d27bd000000'), u'author': u'Mike',
    u'tags': [u'mongodb', u'python', u'pymongo']}
{u'date': datetime.datetime(2009, 11, 12, 11, 14), u'text': u'Another post!',
    u'_id': ObjectId('4f2dcf0e856f1d27bd000001'), u'author': u'Mike',
    u'tags': [u'bulk', u'insert']}
{u'date': datetime.datetime(2009, 11, 10, 10, 45), u'text': u'and pretty easy too!',
    u'_id': ObjectId('4f2dcf0e856f1d27bd000002'), u'author': u'Eliot',
    u'title': u'MongoDB is fun'}

Como fizemos com find_one(), podemos filtrar os resultados com find():

>>> for post in posts.find({"author": "Mike"}):
...     post
...
{u'date': datetime.datetime(2012, 2, 4, 23, 6, 3, 554000), u'text': u'My first blog post!',
    u'_id': ObjectId('4f2dbb9c856f1d27bd000000'), u'author': u'Mike',
    u'tags': [u'mongodb', u'python', u'pymongo']}
{u'date': datetime.datetime(2009, 11, 12, 11, 14), u'text': u'Another post!',
    u'_id': ObjectId('4f2dcf0e856f1d27bd000001'), u'author': u'Mike',
    u'tags': [u'bulk', u'insert']}

Contando

Se queremos apenas saber a quantidade de documentos podemos utilizar o método count():

>>> posts.count()
3

Podemos também aplicar  count() após uma filtragem:

>>> posts.find({"author": "Mike"}).count()
2

Consultas por intervalo

MongoDB suporta diferentes tipos de consultas avançadas. Como exemplo, podemos realizar uma consulta que limita os resultados pelos posts com uma data menor que uma data específica, e com os resultados odenados pelo autor:

>>> d = datetime.datetime(2009, 11, 12, 12)
>>> for post in posts.find({"date": {"$lt": d}}).sort("author"):
...     post
...
{u'date': datetime.datetime(2009, 11, 10, 10, 45), u'text': u'and pretty easy too!',
    u'_id': ObjectId('4f2dcf0e856f1d27bd000002'), u'author': u'Eliot',
    u'title': u'MongoDB is fun'}
{u'date': datetime.datetime(2009, 11, 12, 11, 14), u'text': u'Another post!',
    u'_id': ObjectId('4f2dcf0e856f1d27bd000001'), u'author': u'Mike',
    u'tags': [u'bulk', u'insert']}

Aqui foi utilizado o operador especial "$lt" e também uma chamada a sort() para ordenar os resultados pelo autor.

Indexação

Para tornar a consulta anterior mais rápida podemos criar um compound index (índice composto) em "date" e "author". Para começar, vamos utilizar o método explain() para obter algumas informações sobre como a consulta está sendo realizada sem o índice:

>>> posts.find({"date": {"$lt": d}}).sort("author").explain()["cursor"]
u'BasicCursor'
>>> posts.find({"date": {"$lt": d}}).sort("author").explain()["nscanned"]
3

Podemos ver que a consulta utilizou BasicCursor e passou por todos os 3 documentos da coleção. Vamos adicionar o índice e depois obter a mesma informação:

>>> from pymongo import ASCENDING, DESCENDING
>>> posts.create_index([("date", DESCENDING), ("author", ASCENDING)])
u'date_-1_author_1'
>>> posts.find({"date": {"$lt": d}}).sort("author").explain()["cursor"]
u'BtreeCursor date_-1_author_1'
>>> posts.find({"date": {"$lt": d}}).sort("author").explain()["nscanned"]
2

Agora a consulta está utilizando um BtreeCursor (índice) e passando apenas por 2 documentos. Em bancos muito maiores que esse, isso fará bastante diferença :)

Por fim, fechamos a conexão:

>>> connection.close()

Mais informações sobre PyMongo podem ser vistas aqui e a documentação do MongoDB aqui. Mais exemplos podem ser vistos aqui.

Este post é resultado da tradução e adaptação de:

No related posts.

One Comment
  1. Ótimo texto cara. Muito bom mesmo…
    Continue o bom trabalho!

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS