MongoDB em AdvPL – Parte 01

Introdução

O post anterior — A abordagem NoSQL — abordou apenas uma introdução ao assunto, já os posts MongoDB em AdvPL – Prova de Conceito e MongoDB em AdvPL – JSON e BSON dão inicio a uma implementação Client em AdvPL para o MongoDB. A partir desse post, vamos ver por dentro e por fora a transformação da prova de conceito em uma classe Client de acesso ao MongoDB.

Revisando a visão geral do MongoDB

Apenas revisando alguns pontos já vistos anteriormente … O banco de dados MongoDB é basicamente orientado a documentos. Chamamos de “collection” (ou coleção)  um agrupamento de documentos — apenas outro nome para uma “tabela“. Cada documento é um objeto JSON, flexível por natureza para armazenar documentos complexos com informações dispostas hierarquicamente como uma árvore. Um Objeto JSON pode conter um ou mais objetos JSON como propriedades.

O MongoDB possui uma interface de linha de comando interativa com sintaxe de instruções JavaScript — chamada de “mongo Shell” — onde podemos realizar inúmeras operações em modo interativo com o banco de dados e APIs Client para acesso usando inúmeras linguagens. O ponto em comum do Shell e das APIS são as instruções e operações. Tanto o Shell como as APIs do MongoDB disponibilizam uma forma de executar um comando — db.runCommand() — e através dela podemos executar todas as operações disponíveis do MongoDB. As operações básicas de criação, leitura, atualização e deleção são realizadas através dos comandos insert, find, update e delete, respectivamente,

A camada de comunicação TCP oferecida pelo MongoDB — chamada de Wired Protocol — oferece um mecanismo de mensagens para  execução de comandos, e como vimos no post anterior, podemos consumir essa camada diretamente no AdvPL usando uma classe de conexão TCP  — tSocketClient — mas para usufruir de forma elegante e intuitiva destas funcionalidades, precisamos construir uma interface client do Banco de Dados que encapsule as funcionalidades do banco, e seus respectivos tratamentos.

Criar uma API Client ou Driver

A prova de conceito realizada no outro post apenas prova que é possível de ser feito. A questão em foco, de “como fazer”, é um pouco mais ampla do que apenas trocar mensagens TCP com o MongoDB. Num primeiro momento, o objetivo é consumir uma instância local do banco de dados, com um database e uma tabela (collection), e ler e gravar documentos nela.

Isso não é tão complicado assim, mas vale lembrar que a relação entre COMANDOS e FUNCIONALIDADES não é 1:1 (um para um) , uma funcionalidade como realizar uma busca retornando um cursor de resultados é uma funcionalidade com pelo menos cinco ou seis métodos, que por baixo do capô vai usar dois ou três comandos na camada de comunicação do MongoDB.

Classe ZMONGODBCLIENT

A implementação inicial — ainda em desenvolvimento — da API Client do MongoDB parte da classe AdvPL ZMONGODBCLIENT — implementada no Projeto ZLIB — Fontes do GITHUB.

O código inicial parte da prova de conceito, encapsulando a conexão com o MongoDB e a interface de execução de um — leia-se “qualquer” — comando usando esta conexão. Ele vai servir de base para todas as demais implementações. Seus métodos principais são apenas três:

  • Connect()
  • Disconnect()
  • RunCommand()

Por dentro, a mágica mais importante está na implementação da conversão de um objeto JSON AdvPL para uma string binária BSON, e vice-versa. A execução de um comando no MongoDB exige uma requisição no formato BSON — ou Binary JSON. Nada mais prático do que aproveitar o objeto/classe JsonObject() nativo do AdvPL e criar um conversor 😀

A prova de conceito publicada no post anterior tinha por volta de 100 linhas, e não tratava o retorno da camada de comunicação, apenas recebia e mostrava um “dump” do protocolo no console, executando apenas o comando “buildInfo” no MongoDB  . Usando a classe ZMONGODBCLIENT, ela ficaria assim:

#include 'protheus.ch'
#include 'zlib.ch'

User Function MongoTst()
Local oMongoCli
Local oCmd
Local oResponse

// Conexão com o MongoDB 
oMongoCli := ZMONGODBCLIENT():New()
IF oMongoCli:Connect()

  // Comando a ser executado 
  oCmd := JsonObject():new()
  oCmd['buildInfo'] := 1

  // Chama o comando no MongoDB 
 // O resultado será um objeto JSON 
  oResponse := oMongoCli:RunCommand('buildInfo',oCmd)

  // Mostra o Objeto JSON como string no console
  conout('BuildInfo .....: '+oResponse:ToJson())

  // Desconecta do banco 
  oMongoCli:Disconnect()

Else
   MsgStop("MongoDB Connection Error")
Endif

Return

Em menos de 30 linhas eu abro uma conexão com o banco local, submeto um comando, e o retorno já chega como um objeto JSON do AdvPL. Porém, por baixo do capô, a classe ZMONGODBCLIENT têm pouco mais de 600 linhas, embutindo os tratamentos e conversões BSON x JSON.

Inserção de documentos

Com a classe de conexão e execução básica já funcional, já é possível criar um exemplo completo de CRUD usando o MongoDB, por hora construído para ser executado em uma instância local do MongoDB, sem nenhuma configuração especial ou adicional, nem mesmo autenticação de usuário. Como meu ambiente é de desenvolvimento e testes, eu sequer preciso criar explicitamente um database ou uma tabela / collection. O banco simplesmente cria o database e a collection uma vez que eu insira um documento. Vamos para o exemplo que fica mais fácil:

USER Function MongoIns()
Local oMongoCli, oCmd, oDoc , oResponse

// Conexão com o MongoDB 
oMongoCli := ZMONGODBCLIENT():New()
oMongoCli:Connect()
oMongoCli:SetDB('teste') // Nome do database a criar/usar

// Inserção de um documento
oCmd := JsonObject():new()
oCmd['insert'] := 'pessoas' // nome da Collection ou tabela

// Cria o Documento a inserir 
oDoc := JsonObject():new()
oDoc['id'] := 1 
oDoc['nome'] := 'John Connor'
oDoc['terminated'] := .T.

// Coloca o documento no array de inserção 
oCmd['documents'] := { oDoc }

// Chama o comando no MongoDB 
oResponse := oMongoCli:RunCommand('insert',oCmd)

// Mostra o Objeto JSON como string no console
conout('Response .....: '+oResponse:ToJson())

// Desconecta do banco 
oMongoCli:Disconnect()

Return

// Resultado esperado no log de console

Response .....: {"n":1,"ok":1}

Certo, agora vamos por partes. Antes de mais nada eu consultei a documentação do comando de inserção — insert – MongoDB Manual —  e vi que o mínimo necessário para fazer uma inserção é informar o nome de uma collection (ou tabela) sob a qual o(s) documento(s) serão catalogados, e um ou mais documentos na forma de um ARRAY JSON. Para cada comando também está documentado o retorno esperado. Todos os retornos possuem uma propriedade “ok”, numérica, onde 1 indica sucesso e 0 indica falha. Uma inserção com sucesso retorna apenas “ok” = 1 e “n” informando o número de documentos inseridos.

Atualização de documentos

Começando pela documentação do comando update – MongoDB Manual,  constatamos que a estrutura do comando é um pouco mais complexa, pois permite desde a substituição de um documento, até a atualização de valores em múltiplos documentos. Vamos partir do exemplo que eu quero trocar apenas o nome do registro inserido no programa de exemplo de inserção.

USER Function MongoUpd()
Local oMongoCli, oCmd, oUpd , oSet , oResponse

// Conexão com o MongoDB 
oMongoCli := ZMONGODBCLIENT():New()
oMongoCli:Connect()
oMongoCli:SetDB('teste')

// Update de um documento
oCmd := JsonObject():new()
oCmd['update'] := 'pessoas' // nome da Collection ou tabela

// Cria uma instrução de update 
oUpd := JsonObject():new()

// Cria a "query" com a condição para update 
// Atualizar apenas o documento cujo 'id' seja igual a 1 
oUpd['q'] := JsonObject():New()
oUpd['q']['id'] := 1

// Cria o update set 
// no nosso exemplo, vamos apenas atualizar um valor,

oSet := JsonObject():New()
oSet['nome'] := 'Sarah Connor'

// Adiciona o SET na instrução de update 
oUpd['u'] := JsonObject():New()
oUpd['u']['$set'] := oSet

// Coloca a instrução no array de updates 
oCmd['updates'] := {oUpd}

// Chama o comando no MongoDB 
oResponse := oMongoCli:RunCommand('update',oCmd)

// Mostra o Objeto JSON como string no console
conout('Response .....: '+oResponse:ToJson())

// Desconecta do banco 
oMongoCli:Disconnect()

Return

// resultado esperado 
Response .....: {"n":1,"nModified":1,"ok":1}

Novamente, vamos por partes. A instrução acima está parametrizada para atualizar apenas um documento, substituindo o valor da propriedade ‘nome’, apenas para os documentos onde o campo ‘id’ seja igual a um. Se você quer atualizar mais de um documento, você precisa informar uma propriedade adicional — oUpd[‘multi’] := .T. — na instrução de update. Se este flag não for setado, mesmo que você coloque uma condição de atualização que afete mais de um registro, apenas o primeiro registro encontrado será atualizado. O comando de update possui funções adicionais para atualizar, incrementar, definir, remover campos, entre outras.

Localizando documentos

Usamos o comando find — MongoDB Manual  para localizar e recuperar um ou mais documentos que atendam a um critério de busca. A instrução pode ser parametrizada de várias formas, para retornar apenas um documento, ou para retornar múltiplos documentos em uma única requisição, ou ainda múltiplos documentos em blocos de múltiplas requisições usando o comando getMore. No exemplo abaixo, vamos usar a parametrização mais simples, para buscar apenas um documento que atende  a um critério de busca, usando o comando find de modo “direto”.

User Function MongoFind()
Local oMongoCli, oCmd, oResponse

// Conexão com o MongoDB 
oMongoCli := ZMONGODBCLIENT():New()
oMongoCli:Connect()
oMongoCli:SetDB('teste')

// Monta o comando find
oCmd := JSONOBJECT():new()
oCmd['find'] := 'pessoas'
oCmd['limit'] := 1

// Monta o filtro para selecionar apenas um documento 
oCmd['filter'] := JsonObject():New()
oCmd['filter']['id'] := 1

// Submete o comando
oResponse := oMongoCli:RunCommand('find',oCmd)

// Mostra o Objeto JSON como string no console
conout('Response .....: '+oResponse:ToJson())

// Desconecta do banco 
oMongoCli:Disconnect()

return

// Retorno esperado ( formatado para o post ) 

Response .....: {
  "cursor":{
    "firstBatch":[
      {"terminated":true,"_id":"#OBJID_5DD03AA935D2D914C8737892","id":1,"nome":"Sarah Connor"}
    ],
    "ns":"teste.pessoas",
    "id":"#INT64_0000000000000000"
  },
  "ok":1
}

Por hora vamos reparar nas partes importantes do retorno. Um JSON com a propriedade  “cursor”, dentro dela o objeto de retorno “firstBatch” contém um array de objetos com apenas um documento — encontrado pela busca. O documento possui um campo a mais, chamado “_id”, criado pelo MondoGB para ser um identificador único para este documento, e o objeto ‘cursor’ também têm uma propriedade ‘id’, representada em um inteiro de 64 bits, que nesse exemplo veio totalmente zerado. Se uma requisição de find recupera múltiplos documentos, e eles não são recuperados em uma única requisição, o MongoDB mantém um cursor aberto com esse identificador, para você continuar a recuperar os objetos dessa busca em requisições subsequentes, usando o comando getMore. Vamos ver isso no próximo exemplo, já usando um encapsulamento de QUERY em AdvPL para o MongoDB.

QUERY / CURSOR no MongoDB — Classe ZMONGOQUERY

No post anterior, já existia um exemplo meramente didático — não funcional — de recuperação de múltiplos objetos usando os comandos find / getMore. Agora, com a classe ZMONGOQUERY, podemos recuperar múltiplos objetos de forma sequencial, com os tratamentos encapsulados de tal forma, que seu uso fica praticamente igual a um cursor / result set em um Database SQL 😀

USER Function MongoQry()

Local oMongoDB
Local oQry
Local nRecords := 0

oMongoDB := ZMONGODBCLIENT():New()
oMongoDB:SetVerbose(.T.)
oMongoDB:Connect()
oMongoDB:SetDB("teste")

oQry := ZMONGOQUERY():New('pet001',oMongoDB)
oQry:SetProperty('limit',10)
oQry:AddFilter('BAIR','$eq','Morumbi')
oQry:AddOrder('IDPROP')
oQry:Open()

While !oQry:Eof()
   nRecords++
   oObject := oQry:GetObject()
   conout("Object ("+cValToChar(nRecords)+") : "+oObject:ToJson())
   oQry:Skip()
Enddo

conout("Records .. "+cValToChar(nRecords))

oQry:Close()
oMongoDB:Disconnect()

Return

A intenção do programa acima é retornar os 10 primeiros objetos de um cadastro, onde a propriedade “BAIR” dos objetos seja igual a “Morumbi”, ordenados pelo campo / propriedade “IDPROP”.  Para isso, eu utilizo diretamente a classe ZMONGOQUERY, informando no construtor qual é a collection a ser pesquisada, e informando também o objeto da conexão com o MongoDB. Depois, defino a propriedade ‘limit’ de busca — afinal internamente serão usados o find/getMore — depois acrescento a condição de busca e a ordenação, encapsulados pelos métodos AddFilter e AddOrder, e então uso uma abordagem sintática equivalente a um cursor de objetos — com os métodos GetObject(), Skip() e EOF(). para recuperar o objeto atual do cursor, mover o cursor para o próximo objeto, e verificar se ainda existem objetos no cursor — respectivamente.

Com pequenas alterações, por exemplo trocando o nome da collection no construtor de “pet001” para “pessoas”, e removendo as linhas que setam as condições de filtro e ordenação, eu obtenho um fonte que abre um cursor e recupera um a um todos os objetos da coleção ‘pessoas’ do database ‘teste’.

Conclusão

Já temos um CRUD elegante em AdvPL com o MongoDB, e uma API capaz de rodar “quase” qualquer coisa, agora o desafio é encapsular mais algumas funcionalidades interessantes do MongoDB e refinar os tratamentos de erro. Para rodar estes fontes no seu ambiente, você precisa apenas baixar a ZLIB do GITHUB, acrescentar, e usar um TOTVS Application Server que já tenha suporte ao objeto JSON nativo do AdvPL. Todos os fontes acima atualmente rodam em uma build Windows 64 bits atualizada do TOTVS Application Server “Lobo Guará” 😀 Teremos mais novidades e recursos nos próximos posts !!!

Agradeço novamente a audiência e desejo a todos TERABYTES DE SUCESSO !!!

Referências

2 comentários sobre “MongoDB em AdvPL – Parte 01

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google

Você está comentando utilizando sua conta Google. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s