FredZvt.WriteLines();

No-SQL – MongoDB – Da introdução a utilização em alto nível em C# com NoRM.

with 7 comments

Click here to read the english version of this article.

Neste artigo tentarei fazer uma introdução ao movimento No-SQL, a base de dados orientada a documentos MongoDB e três formas de utilizá-la: via shell, via o driver para C# mongodb-csharp e via a biblioteca NoRM.

O objetivo deste artigo é que o leitor tome conhecimento do que é e de como utilizar de forma básica o MongoDB, para isso os exemplos de utilização que percorreremos são intencionalmente simples e não visam explorar as diversas formas de se executar uma operação no MongoDB tampouco demostrar toda a sua capacidade.

Por favor, tenham em mente de que eu ainda estou estudando o software e ainda não possuo experiência prática em projetos utilizando o MongoDB, tendo meu conhecimento limitado por meus estudos particulares e, embora tenha feito todo o esforço para garantir em fontes confiáveis a veracidade das informações até o momento de sua escrita e testado em meu computador todos os exemplos de códigos aqui citados não posso dar maiores garantias. Críticas construtivas e correções serão extremamente bem vindas.

Conteúdo

No-SQL

Existem hoje várias bases de dados que rompem com os requisitos clássicos atendidos pelos sistemas gerenciadores de bases de dados relacionais e tem como principais características:

  • O não-uso de SQL como API de consulta. (exemplos de APIs utilizadas incluem JSON, BSON, REST, RPC, etc.)
  • Não garantem operações atômicas (não-ACID).
  • Distribuídas e escaláveis horizontalmente.
  • O abandono aos esquemas pré-definidos.
  • O armazenamento de dados em um modelo não-tabular (ex: key-value, object, graphs, etc.)

OBS: Nem todos os bancos de dados categorizados como No-SQL possuem todas essas características, uns possuem umas, outros outras, alguns todas.

O esforço da comunidade de desenvolvimento e evangelização dessas novas abordagens tecnológicas foi nomeado de No-Sql e vem fazendo muito barulho desde o início de 2009. No-SQL deveria ser entendido como “Not only SQL” (Não somente SQL) apesar dessa interpretação não ser muito óbvia, em minha opinião. Faria mais sentido pra mim se tivesse sido rotulada de No-Relational.

A listagem mais completa de bases de dados que podem ser enquadradas como parte do movimento No-Sql que eu encontrei está em http://nosql-database.org/. Quando escrevi este artigo, haviam 47 lá distribuídas entre diversas categorias.

No blog Escalabilidade existe uma introdução bacana de bases No-SQL sob o ponto de vista de escalabilidade.

Aqui foco em MongoDB, que junto com CouchDB foram as que mais me interessaram desde que comecei a pesquisar sobre o assunto.

No-SQL ou bases relacionais!? Os dois!

Chamo atenção para que não pensem que bases não relacionais representam uma substituição as bases relacionais. O ideal é que você conheça os prós e contras de cada software e os utilize da melhor forma possível nos cenários que encontrar.

Acredito que o cenário de geração de relatórios computados sobre uma grande base de dados seja o alvo mais óbvio de críticas. Essas situações são resolvidas de forma excelente dando bom uso as bases de dados relacionais e sistemas de Data Warehouse agregando de forma frequente os dados armazenados em bases não relacionais dando suporte as aplicações de produção.

Rob Conery escreveu um excelente artigo sobre este tópico.

MongoDB

MongoDB é uma base de dados escalável de alta performance orientada a documentos com esquema dinâmico que tenta apresentar o melhor dos mundos entre os sistemas de armazenamento baseados em chaves/valores (key/values) onde os valores são estruturados como documentos JSON e os sistemas gerenciadores de bancos de dados relacionais atuais com recursos como vários modelos de indexação e consultas dinâmicas.

Seu nome, cá para nós brasileiros, soa muito mal. Mas, em inglês, vem de “humongous” (algo gigante), talvez com tom sarcástico por ser um software tão pequeno e com recursos tão poderosos ou talvez por ser escalável até se tornar algo realmente monstruoso. Vai saber…

Hoje é usado em produção por pelo menos 40 sites de alto acesso e performance, incluindo sites como SourceForge, GitHub, EA e The New York Times.

MongoDB vs CouchDB

Apesar de MongoDB e CouchDB apresentarem diferenças em suas abordagens de implementação em aspectos tais como versionamento e controle de concorrência, escalabilidade, API de consulta, etc, e ainda que ambos sejam projetos de código livre, ainda os considero como softwares concorrentes por oferecerem recursos similares. Acredito que o MongoDB leve uma grande vantagem e irá se desenvolver mais rapidamente visto que é escrito em C++ e não em Erlang, como o CouchDB.

Instalação e inicialização do servidor do MongoDB

O MongoDB pode ser baixado em versões compiladas para Linux, OS X e Windows. Para os exemplos deste artigo vou usar a versão 1.4.0 compilada para Windows 64-bits mas acredito não haver diferenças usando a versão 32-bits.

Quando terminar de baixar o arquivo zip, descompacte-o em uma pasta qualquer de seu computador. Serão criadas quatro diretórios: bin, include, lib e lib64. As pastas include, lib e lib64 são utilizadas para quem for utilizar o MongoDB programando em C++, no nosso caso o que interessa é a aplicação mongod.exe dentro da pasta bin, que é o servidor do MongoDB.

Por padrão, o MongoDB utiliza a pasta c:\data\db como caminho padrão para suas bases de dados mas essa pasta não é criada automaticamente, portanto crie-as antes de executar o servidor. Se preferir usar outra pasta para o armazenamento das bases, inicialize o servidor com o parâmetro ––dbpath [caminho da pasta]. Eu, particularmente, gosto de utilizar também o parâmetro ––directoryperdb que faz com que o MongoDB crie uma subpasta para cada base que for criada. Enfim, executando o mongod.exe o servidor será inicializado por padrão em localhost, na porta 27017.

Console com o mongod.exe iniciado e esperando por conexões.

A seguir, executaremos as quatro operações básicas de manipulação de dados (CRUD) e consultas simples em uma base de testes no MongoDB utilizando o Mongo shell, em C# com o driver mongdb-csharp e em alto nível utilizando a biblioteca NoRM.

Utilizando o MongoDB com o Mongo shell

O executável mongo.exe é o Mongo shell. Execute-o sem parâmetros e o shell inicializará conectado ao servidor MongoDB em sua porta padrão (27017).

Entre com o comando show dbs e serão listadas as bases de dados que existem: inicialmente admin, local e test. Por padrão o shell inicializa configurado para utilizar a base test. Para alterar a base que receberá nossos comandos, utilize o comando use [nome da base]. A criação de novas bases se dá automaticamente ao posicionarmos o shell em uma base não existente e inserirmos algum dado nela.

Vamos agora inserir nosso primeiro documento em nossa nova base, efetivamente criando-a. Entre com o comando use minhabase, isso irá posicionar o shell na base inexistente minhabase. Neste momento a base ainda não existirá, você pode confirmar isso utilizando o comando show dbs novamente. Agora vamos usar o comando db.[nome da coleção].save([documento JSON]) para criar e salvar um documento, note que o documento deverá ser escrito utilizando a notação de objetos em Javascript (JSON). Entre com o comando: db.Pessoas.save({ nome: "Fred", idade: 28 }) e agora com o comando db.Pessoas.save({ nome: "Carlos", idade: 30 }). Pronto, agora temos uma base de nome minhabase com uma coleção de nome Pessoas e dois documentos JSON nessa coleção: um representando uma pessoa de nome Fred com 28 anos e outro representando uma pessoa de nome Carlos de 30 anos.

Existem duas limitações extras em documentos JSON escritos para serem armazenados pelo MongoDB: os nomes das chaves não poderão iniciar com $ ou terem . (pontos) em qualquer lugar. Portanto, documentos como { $teste : 1 } ou { "teste.chave" : "valor" } serão inválidos.

Uma das características mais interessantes de bases de dados No-SQL orientadas a documentos é a não restrição de esquemas em uma coleção. Em outras palavras, uma coleção pode ter documentos cujas estruturas sejam completamente diferentes. Para quem está acostumado com bancos de dados relacionais onde cada documento (registro) só pode ter o formato pré-definido pela tabela que o contém, esta é uma grande mudança de paradigma. Para exemplificar isso, vamos adicionar um novo documento em nossa coleção que não se estruture como os dois anteriores que incluímos. Entre com o comando db.Pessoas.save({ primeironome: "Joaquim", sobrenome:"Silva", idade: "15 anos" }). Com este comando acabamos de inserir na coleção Pessoas um documento com uma estrutura completamente diferente dos documentos anteriores e é absolutamente válido.

Uma vez com os dados armazenados, o próximo passo será consultar esses dados. No shell, as consultas são realizadas através do comando db.[nome da coleção].find(...). Se executarmos este comando sem nenhum parâmetro ele irá listar todos os documentos de uma coleção, portanto db.Pessoas.find() irá retornar os três documentos que você acabou de inserir. O comando find pode ser utilizado de muitas formas, nos permitindo executar quaisquer consultas que imaginarmos em nossas coleções. Esse é um dos excelentes recursos do MongoDB em que ele se destaca de várias outras bases No-SQL. Para exemplificarmos um pouco (bem pouco mesmo!) este recurso, vamos executar uma consulta com um critério simples: para retornar somente o documento que representa a pessoa chamada Fred executaríamos db.Pessoas.find({ nome: "Fred" }). O comando find, além de outros comandos do MongoDB recebem como parâmetro um documento JSON como uma especificação de critérios para que o comando somente se aplique aos objetos da coleção que possuem propriedades que o satisfazem.

Note que o documento retornado possui uma propriedade além das que declaramos explicitamente: a propriedade _id. Todo documento armazenado no MongoDB possui um _id que será único para cada documento em uma coleção. Podemos declará-lo explicitamente na criação do documento ou o MongoDB irá criar automaticamente um objeto do tipo ObjectID e atribuir ao mesmo. Essa propriedade constitui o principal identificador de um documento no MongoDB e será idexado por padrão em todas as coleções. A propriedade _id não tem obrigatoriedade de ser um ObjectID, podendo ser de qualquer tipo suportado por documentos JSON (na verdade BSON), mas o uso de identificadores ObjectID é recomendado por facilitar a escalabilidade de bases MongoDB.

Para excluir um documento da coleção, utilizaremos o comando db.[nome da coleção].remove([documento-critério JSON]), portanto, para removermos o documento que representa a pessoa chamada Carlos, executaríamos db.Pessoas.remove({ nome: "Carlos" }).

Para atualizarmos um documento da coleção, vamos antes buscar o objeto que desejamos alterar, modificar uma de suas propriedades e salvá-lo novamente na base, efetivamente atualizando-o. O shell nos disponibiliza um comando muito útil para quando precisamos obter um único documento de uma coleção para manipulá-lo: o comando db.[nome da coleção].findOne([documento-critério JSON]), portando executando o comando joaquim = db.Pessoas.findOne({ primeironome: "Joaquim" }); obteremos na variável joaquim uma instância do documento para manipulação. Agora podemos alterá-lo e salvá-lo novamente na coleção: execute joaquim.sobrenome = "Souza"; para alterar seu sobrenome e db.Pessoas.save(joaquim); para atualizar o documento na base de dados.

Ao meu ver, o shell é a principal interface de administração do MongoDB, embora seu uso principal seja através de drivers específicos por linguagem de programação. Todos os recursos do MongoDB podem ser acessados através do shell, tornar-se íntimo dessa ferramente ajudará muito o seu trabalho com bases de dados MongoDB. Veja a documentação do shell no site oficial do MongoDB.

Utilizando o MongoDB com o mongodb-csharp

Assim como o shell é a interface entre humanos-MongoDB os drivers são as interfaces entre softwares-MongoDB. Hoje existem drivers para 25 linguagens de programação. Somente para a framework .NET existem 6 drivers/ferramentas, segundo Bryan Migliorisi em um artigo em seu blog e, dentre esses, o mongodb-csharp, desenvolvido por uma trupe liderada por Sam Corder, é o mais maduro até o momento.

Uma pequena crítica ao projeto vai apenas para a documentação precária, acho que ao menos o wiki do projeto deveria conter alguns exemplos dos recursos porém é possível entender o funcionamento da maioria deles analisando os projetos de testes. Acredito que isso irá melhorar com o tempo pois o projeto ainda é bastante embrionário.

Neste artigo usaremos a versão 0.82.2 do mongodb-csharp para executar nossas operações de testes em nosso código. Sua API é bastante similar a do shell, tornando ainda mais fácil a nossa vida.

Descompactando o arquivo do download, teremos três bibliotecas dinâmicas: MongoDB.Driver.dll, MongoDB.GridFS.dll e MongoDB.Linq.dll. A que devemos adicionar como referência em nosso projeto é a MongoDB.Driver.dll. As outras duas são, respectivamente, uma API para a utilização do suporte à especificação GridFS, que permite o armazenamento de objetos binários com mais de 4MB no MongoDB e o suporte básico a LINQ.

A primeira coisa que devemos fazer em nosso código para utilizar o MongoDB é conectar ao servidor:

using MongoDB.Driver;

Mongo mongo = new Mongo();
mongo.Connect();

Em seguida, devemos obter uma referência à base de dados e à coleção que queremos manipular:

Database db = mongo.GetDatabase("minhabase");
IMongoCollection collection = db.GetCollection("Pessoas");

A classe MongoDB.Driver.Document representa um documento do MongoDB e funciona de forma muito semelhante a um System.Collections.Generic.Dictionary<String, Object>:

Document doc = new Document();
doc.Add("nome", "Guilherme");
doc.Add("idade", 22);

Para persistir um novo documento na coleção referenciada:

collection.Save(doc);

Quando executamos o comando find no shell o que retornamos é, na verdade, um cursor que é iterado automaticamente pelo shell mas que poderíamos referenciar em uma variável e manipular manualmente. O mongodb-csharp utiliza o mesmo princípio, mas aqui o trabalho de iteração sobre os valores será sempre manual.

ICursor cur = collection.FindAll();
foreach (Document iDoc in cur.Documents)
{
    Console.WriteLine("Nome: {0}, Idade: {1}", iDoc["nome"], iDoc["idade"]);
}

Para atualizarmos um documento, o processo é, mais uma vez, semelhante ao executado no shell: buscar uma referência a um documento específico, alterá-lo e salvá-lo de volta a coleção:

Document doc2 = collection.FindOne(new Document() {{"primeironome", "Joaquim"}});
doc2["sobrenome"] = "Silva";

collection.Save(doc2);

E, por fim, para excluirmos um documento:

collection.Delete(doc2);
collection.Delete(new Document() {{ "nome", "Guilherme" }});

Simples, não!? O driver mongodb-csharp nos proporciona APIs para quase a totalidade de funcionalidades do MongoDB e é muito fácil de ser utilizado, como pudemos ver.

Utilizando o MongoDB com o NoRM

Depois de ter conhecido a biblioteca NoRM, quando utilizo o mongodb-csharp para acessar uma base MongoDB fico com um gosto amargo de baixo nível em minha boca, lembro-me da época em que acessava bancos de dados com Recordsets em ASP clássico.

A biblioteca NoRM proporciona uma abstração em alto nível para a manipulação de documentos em uma base MongoDB de forma fortemente tipada além de disponibilizar uma API mais amigável para envio de comandos a um servidor MongoDB.

NoRM é um projeto de código livre, assim como o mongodb-csharp é hospedado no github, tem Andrew Theken como líder e principal desenvolvedor e contribuidores do peso de Rob Conery (a mente por trás do projeto SubSonic). É um projeto tão novo que nem release tem ainda, mas que já chama muita atenção!

Pelo fato de não ter saído release ainda, somos obrigados a baixar o código para compilarmos em nossa máquina. Na página do projeto no github, clique no botão “Download Source” e, em seguida escolha o formato de compactação que preferir. Salve e descompacte em alguma pasta de seu computador. Ou faça um clone do repositório Git — Tino Gomes ensina como fazer isso no Windows). Abra a solução NoRM.sln, compile o projeto NoRM e referencie a DLL gerada em sua aplicação ou adicione o projeto NoRM/NoRM.csproj a solução da sua aplicação e referencie o projeto adicionado.

A biblioteca NoRM funciona de forma fortemente tipada, fazendo a tradução automática entre documentos BSON armazenados no MongoDB e suas classes em C# e vice-versa, portanto, vamos criar a classe Pessoa:

class Pessoa
{
    public ObjectId ID { get; set; }
    public string Nome { get; set; }
    public int Idade { get; set; }
}

NoRM impõe certas convenções em nossas classes para que a serialização/deserialização seja realizada sem problemas, são elas:

  • Os tipos suportados para serialização/deserialização (até o momento em que este artigo foi escrito) são:
    • int
    • bool
    • double
    • float
      • Sempre será serializado/deserializado como double
    • DateTime
    • string
    • byte[]
    • Norm.ObjectId
    • Regex
    • Guid
    • Enums
      • Deve herdar de uint, int, ulong, long
      • (ex: public enum meu_enum : int {...})

    • Norm.ScopedCode
    • IEnumerable<T>
      • T deve também ser de um dos tipos suportados
      • Sempre será deserializado como List<T> Atualização: Como informou Andrew Theken, parece que agora temos a opção de utilizar outros tipos de coleções.
  • É recomendado que ao usar tipos de valor (value-types) deveremos ancapsulá-los em um System.Nullable<T> pois se o valor da propriedade não existir no documento BSON, será atribuído null. (Ex: usar int? ao invés de int)
  • Todas as classes devem possuir uma propriedade pública chamada _id ou ID sendo de qualquer um dos tipos suportados, porém se forem do tipo Guid ou Norm.ObjectId não precisaremos inicializar este valor em uma instância pois NoRM se encarregará disso.
    • Caso queira utilizar uma outra propriedade qualquer como identificador de seu documento poderá utilizar o atributo [MongoIdentifier] para tal, livrando-se da obrigatoriedade de criar uma propriedade _id ou ID
  • Todas as propriedades públicas deverão possuir acessores get e set. Atualização: Como informado por Andrew Theken, o trabalho de Karl Seguin tornou possível a utilização de propriedades públicas com get e set private ou protected.
  • NoRM ainda não reconhece referências cíclicas entre classes deixando por conta do programador tomar cuidado com isso, caso contrário a execução da aplicação entrará em loop infinito.
  • NoRM ainda não resolve casos de tentativa de deserializações de tipos inválidas, por exemplo, se tentarmos deserializar um valor BSON inteiro em uma propriedade DateTime um erro de conversão será gerado.
  • O tamanho do documento BSON não poderá ultrapassar 4MB. (Na verdade uma limitação de qualquer documento armazenado no MongoDB que não use a especificação GridFS)
  • Os tipos DateTime terão precisão de milisegundos. Se presisarmos de mais precisão deveremos utilizar double.

Sei que a primeira vista parecem muitas restrições mas ao meu ver, para um projeto cujo primeiro commit data de 30 de janeiro de 2010, é um feito e tanto! Lembre-se de que, se precisarmos de algo que a ferramenta não ofereça recurso poderemos criá-los e submeter um patch ao projeto. Além disso, tudo o que preciso para criar minhas entidades de domínio em POCOs está sendo atendido.

Com tudo em seu lugar, vamos abrir uma conexão com o MongoDB e buscar uma referência a nossa coleção de pessoas:

using Norm;
using Norm.Collections;

Mongo mongo = new Mongo("minhabase");
MongoCollection<Pessoa> Collection = mongo.GetCollection<Pessoa>("Pessoas");

Para inserirmos uma nova pessoa em nossa coleção bastará criar uma instância de Pessoa e salvá-la em nossa coleção:

Pessoa Bruno = new Pessoa();
Bruno.Nome = "Bruno";
Bruno.Idade = 35;

Collection.Save(Bruno);

Para atualizar um documento, buscamos, desta vez de forma fortemente tipada, a instância de Pessoa que queremos alterar passando como parâmetro de busca um tipo anônimo somente com a propriedade Nome, alteramos e salvamos novamente na coleção:

Pessoa Bruno2Update = Collection.FindOne(new { Nome = "Bruno" });
Bruno2Update.Idade = 40;
Bruno2Update.Nome = "Bruno Garcia";

Collection.Save(Bruno2Update);

Para listarmos as pessoas da coleção lindamente:

IEnumerable<Pessoa> Pessoas = Collection.Find();
foreach (Pessoa p in Pessoas)
{
    Console.WriteLine("Nome: {0}, Idade: {1}", p.Nome, p.Idade);
}

E, por último, para excluirmos uma pessoa da coleção, nada mais simples:

Collection.Delete(Bruno2Update);

Atualizações após a publicação deste artigo

A grande aceitação deste artigo foi uma grande surpresa para mim e eu gostaria de agradecer a todos que me deram algum tipo de feedback.

Gostaria de adicionar algumas notas e correções enviadas pelos leitores:

Antes de tudo, gostaria de deixar claro que neste artigo eu não dou preferência ao driver mongodb-csharp nem a biblioteca NoRM. Para mim ficou claro que as duas bibliotecas possuem diferentes propostas. Enquanto mongodb-csharp provê uma API de uma maneira mais similar ao shell, nos dando grande flexibilidade, NoRM se provê maior produtividade com uma abordagem um pouco mais distante do shell mas mais próximo das necessidades reais dos desenvolvedores, provendo um mapeamento direto das classes em C# de forma fortemente tipada, porém menos flexível.

Fiquei feliz em saber pelo Craig Wilson que logo teremos uma versão do mongodb-csharp que trás todos os recursos disponíveis pelo NoRM e algo mais.

Se o projeto mongodb-csharp se propôr a oferecer todos os recursos oferecidos pelo projeto NoRM e vice versa, a princípio eu daria preferência ao projeto mongodb-csharp visto que este está numa fase mais madura. Mas nesse caso não entendo porquê as duas equipes não se juntam e desenvolvem uma única biblioteca que traga o melhor dos dois mundos.

E, finalmente, informo que podem haver atualizações no corpo do artigo para que possa informar melhor os leitores das informações que tenho recebido. Onde houver uma correção, tacharei o texto e farei comentários.

Written by Frederico Zveiter

10 abr - 2010 às 22:35

Publicado em MongoDB, No-SQL, NoRM

Tagged with , , , , , ,

7 Respostas

Subscribe to comments with RSS.

  1. Muito bom o artigo. Deu até vontade de tentar voltar a escrever algo em C# para usar uma base noSQL.
    Confesso que gosto muito do CouchDB embora ainda não tenho me aventurado em nada com o MongoDB.
    Acho que pelo fato de conhecer mais gente que mexe com o couch do que outro.

    aoqfonseca

    11 abr - 2010 at 12:53

  2. Excelente post, parabens!

    Alexandre Valente

    11 abr - 2010 at 15:13

  3. Ótimo, deu pra ter uma boa ideia de como ele funciona. Valeu!

    Giovanni Bassi

    22 abr - 2010 at 02:31

  4. Valeu pelo feedback, pessoal!

    fredzvt

    22 abr - 2010 at 09:08

  5. […] um comentário » Clique aqui para ler a versão original desse artigo em […]

  6. valeu pelo post…

    Gabriel

    25 nov - 2010 at 09:00


Deixar mensagem para Alexandre Valente Cancelar resposta