Pular para o conteúdo principal

MongoDB

Introdução

MongoDB é uma base de dados NoSQL orientada a documentos. Em vez de tabelas e linhas os dados são guardados em documentos no formato semelhante ao JSON, o que permite uma grande flexibilidade no modelo dos dados.

O Netuno oferece o recurso _mongo para a integração com o MongoDB, com uma abstração low-code sobre o cliente oficial do MongoDB para Java, disponível para as diversas linguagens de programação suportadas pelo Netuno: JavaScript, Python, Ruby, Kotlin e Groovy.

Antes de avançar, recomenda-se que saiba como criar serviços no Netuno, veja o tutorial REST - Serviços Web.

Um exemplo de implementação das operações apresentadas aqui está disponível no serviço de exemplo mongo.js que se localiza em:

  • apps/demo/server/services/samples/javascript/mongo.js

Instalação do MongoDB

Com Docker

Assumindo que tem o Docker instalado, basta efetuar o download da imagem do MongoDB e iniciar um novo container:

docker pull mongo
docker run -d --name mongo -p 27017:27017 mongo

Desta forma o MongoDB fica acessível em localhost:27017 sem autenticação.

Para criar o utilizador da base de dados conforme a configuração da aplicação, execute:

docker exec -it mongo mongosh products_db --eval 'db.createUser({ user: "products_user", pwd: "12345678", roles: [{ role: "readWrite", db: "products_db" }] })'

Com Ubuntu

O MongoDB não está incluído nos repositórios oficiais do Ubuntu, portanto é necessário adicionar o repositório oficial antes da instalação:

sudo apt-get install gnupg curl
curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor
echo "deb [ signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] http://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list
sudo apt-get update
sudo apt-get install -y mongodb-org

Depois de instalado, pode iniciar o serviço com:

sudo systemctl start mongod

Com Windows

Execute o instalador descarregado e, no passo Service Configuration, ative Install MongoD as a Service para que o MongoDB arranque automaticamente. O MongoDB fica acessível em localhost:27017 sem autenticação.

Consulte a documentação oficial do MongoDB para outras distribuições e versões.

Configuração da Aplicação

A configuração das conexões do MongoDB é feita no ficheiro de configuração da aplicação, de acordo com o ambiente:

  • config/_development.json

Veja mais sobre os ficheiros de configuração da aplicação.

Dentro do ficheiro de configuração, adicione o bloco mongo, onde cada chave identifica uma conexão:

{
...
"mongo": {
"store": "mongodb://store_user:12345678@localhost/store_db",
"products": {
"username": "products_user",
"password": "12345678",
"host": "127.0.0.1",
"port": "27017",
"database": "products_db"
}
},
...
}

Uma conexão pode ser definida de duas formas:

  • Como URL de conexão — o valor é simplesmente uma string, por exemplo "mongodb://store_user:12345678@localhost/store_db".
  • Como objeto de configuração — com os campos:
    • url — se definido, é utilizado diretamente como URL de conexão, ignorando os outros campos.
    • protocol — o protocolo da conexão, por default mongodb.
    • username e password — as credenciais de acesso.
    • host — o endereço do servidor, por default localhost.
    • port — a porta da conexão, por default 27017.
    • database — o nome da base de dados, por default o nome da chave da conexão.
    • params — parâmetros adicionais da conexão acrescentados à URL, por exemplo { "authSource": "admin" }. Só são aplicados quando a URL é construída a partir dos campos do objeto.

A conexão não tem de ser a base de dados principal da aplicação. Ao contrário da configuração db, o bloco mongo é independente e permite várias conexões em simultâneo.

Inicializar a Conexão

O primeiro passo é inicializar o cliente do MongoDB com o recurso _mongo.init(...), passando o nome da chave de configuração ou diretamente uma URL de conexão:

const mongo = _mongo.init("products")

Também pode inicializar a conexão diretamente com uma URL:

const mongo = _mongo.init("mongodb://localhost:27017/products_db")

Ao chamar _mongo.init() sem argumentos é utilizada a chave de configuração default. É recomendado passar sempre o nome da chave ou a URL, para ficar explícita a conexão utilizada.

Base de Dados e Coleções

Com o cliente inicializado, obtenha a instância da base de dados com database("nome"). Em seguida pode criar coleções e aceder a uma coleção existente:

const db = mongo.database("products_db")

db.createCollection("product")

const collection = db.collection("product")

Pode testar a conexão com a base de dados com o ping(), e listar todas as coleções existentes com collectionNames():

db.ping()

const names = db.collectionNames()

Pode renomear uma coleção com o renameCollection(), passando o nome completo com a base de dados, ou o nome da base de dados e o novo nome da coleção em separado:

collection.renameCollection("products_db.product_archive")

collection.renameCollection("archive_db", "product_archive")

Para remover uma coleção e todos os seus documentos, utilize o drop():

collection.drop()

Inserir Documentos

Os dados são representados com o recurso _val, através de _val.map() para documentos e _val.list() para listas de valores.

Inserir um único documento

Com insertOne() insere um documento e obtém o ID gerado:

const id = collection.insertOne(
_val.map()
.set("name", "Laptop")
.set("quantity", 22)
.set("price", 100)
.set("category", "computers")
)

Os documentos podem conter estruturas aninhadas, como listas e outros documentos:

collection.insertOne(
_val.map()
.set("name", "Tablet")
.set("tags",
_val.list()
.add("promo")
.add("new")
)
.set("details",
_val.map()
.set("color", "blue")
.set("stock", 10)
)
)

Inserir múltiplos documentos

Com insertMany() insere vários documentos de uma só vez, recebendo a lista de IDs gerados:

const ids = collection.insertMany(
_val.list()
.add(
_val.map()
.set("name", "Smartphone")
.set("quantity", 18)
)
.add(
_val.map()
.set("name", "Monitor")
.set("quantity", 8)
)
)

Consultar Documentos

O método find() devolve um MongoFindIterable, a partir do qual obtém os documentos com all() (todos) ou first() (o primeiro, ou null se não houver nenhum).

Para obter todos os documentos da coleção:

const docs = collection.find().all()

Para consultar com um filtro, utilize a factory _mongo.filters():

const docs = collection.find(
_mongo.filters().eq("name", "Laptop")
).all()

Filtros

A factory _mongo.filters() oferece os principais operadores de consulta do MongoDB:

OperadorDescrição
eq("campo", valor)Igual ao valor.
ne("campo", valor)Diferente do valor.
gt("campo", valor)Maior que o valor.
gte("campo", valor)Maior ou igual ao valor.
lt("campo", valor)Menor que o valor.
lte("campo", valor)Menor ou igual ao valor.
in("campo", valor1, valor2, ...)Igual a qualquer um dos valores.
nin("campo", valor1, valor2, ...)Diferente de todos os valores.
and(filtro1, filtro2, ...)Corresponde a todos os filtros.
or(filtro1, filtro2, ...)Corresponde a pelo menos um dos filtros.
not(filtro)Não corresponde ao filtro.
regex("campo", "padrão")Corresponde à expressão regular.
exists("campo")O campo existe no documento.
text("pesquisa")Pesquisa de texto.
type("campo", "tipo")O campo é do tipo especificado.
mod("campo", divisor, resto)O resto da divisão do campo pelo divisor é igual ao valor.
where("expressão")Corresponde aos documentos que satisfazem uma expressão JavaScript, por exemplo "this.price > 100".
size("campo", n)O campo de matriz tem exatamente n elementos.

Por exemplo, para obter os produtos com preço superior a 50 e quantidade inferior a 30:

const docs = collection.find(
_mongo.filters().and(
_mongo.filters().gt("price", 50),
_mongo.filters().lt("quantity", 30)
)
).all()

Ou para obter os produtos cujo nome corresponde a uma expressão regular:

const docs = collection.find(
_mongo.filters().regex("name", "^[LS]")
).all()

Percorrer os Resultados

Com o resultado da consulta você pode percorrer todos os documentos encontrados:

for (const doc of collection.find().all()) {
_out.println(doc.getString("name"))
}

Você também pode percorrer os resultados com forEach() passando uma função:

collection.find().forEach((doc) =>
_out.println(doc.getString("name"))
)

Ordenação

Com a factory _mongo.sorts() você define a ordem dos resultados, de forma ascendente ou descendente:

const docs = collection.find().sort(
_mongo.sorts().descending("price")
).all()

Para ordenar por vários campos, utilize orderBy():

const docs = collection.find().sort(
_mongo.sorts().orderBy(
_mongo.sorts().descending("price"),
_mongo.sorts().ascending("quantity")
)
).all()

Projeção de Campos

Com a factory _mongo.projections() você limita os campos devolvidos nos documentos:

const docs = collection.find().projection(
_mongo.projections().include("name", "quantity")
).all()

Para combinar várias projeções:

const docs = collection.find().projection(
_mongo.projections().fields(
_mongo.projections().include("name", "quantity"),
_mongo.projections().excludeId()
)
).all()

Os tipos de projeção disponíveis são os seguintes:

ProjeçãoDescrição
include("campo", ...)Inclui apenas os campos especificados.
exclude("campo", ...)Exclui os campos especificados.
excludeId()Exclui o campo _id.
fields(projeção1, ...)Combina projeções.
slice("campo", n)Inclui apenas os primeiros n elementos do campo de matriz.
slice("campo", salto, n)Inclui n elementos do campo de matriz, a partir de depois dos primeiros salto elementos.
elemMatch("campo")Inclui apenas o primeiro elemento do campo de matriz.
elemMatch("campo", filtro)Inclui apenas o primeiro elemento da matriz que corresponde ao filtro.
computed("campo", expressão)Adiciona um campo calculado com base numa expressão de agregação.

Limites e Saltos

Com limit() e skip() você controla a quantidade de resultados devolvidos:

const docs = collection.find()
.skip(1)
.limit(2)
.all()

Atualizar Documentos

Com a factory _mongo.updates() você define as alterações a aplicar, e com updateOne() ou updateMany() aplica as alterações nos documentos que correspondem ao filtro:

collection.updateOne(
_mongo.filters().eq("name", "Laptop"),
_mongo.updates().set("quantity", 42)
)

collection.updateMany(
_mongo.filters().eq("category", "computers"),
_mongo.updates().set("category", "featured")
)

A factory _mongo.updates() oferece os operadores de atualização:

OperadorDescrição
set("campo", valor)Define o valor de um campo.
unset("campo")Remove um campo do documento.
rename("campo", "novoNome")Renomeia um campo.
push("campo", valor)Adiciona um valor a um array.
combine(atualização1, atualização2, ...)Combina várias atualizações numa só.

Para aplicar várias alterações de uma só vez, utilize combine():

const combined = _mongo.updates().combine(
_mongo.updates().set("quantity", 42),
_mongo.updates().rename("other", "more")
)

collection.updateOne(
_mongo.filters().eq("name", "Laptop"),
combined
)

Encontrar e atualizar

Com findOneAndUpdate() encontra e atualiza um documento de forma atómica, devolvendo o documento original (ou null se não houver nenhum):

const old = collection.findOneAndUpdate(
_mongo.filters().eq("name", "Laptop"),
_mongo.updates().set("quantity", 42)
)

Substituir Documentos

Com replaceOne() substitui o documento inteiro (ao contrário das atualizações, não pode conter operadores de atualização):

collection.replaceOne(
_mongo.filters().eq("name", "Laptop"),
_val.map()
.set("name", "Laptop")
.set("quantity", 50)
.set("price", 150)
.set("category", "featured")
)

Para encontrar e substituir de forma atómica, devolvendo o documento antigo, utilize findOneAndReplace():

const old = collection.findOneAndReplace(
_mongo.filters().eq("name", "Laptop"),
_val.map()
.set("name", "Laptop")
.set("quantity", 50)
)

Excluir Documentos

Com deleteOne() e deleteMany() exclui os documentos que correspondem ao filtro:

collection.deleteOne(
_mongo.filters().eq("name", "Laptop")
)

collection.deleteMany(
_mongo.filters().eq("category", "inactive")
)

Para encontrar e excluir um documento de forma atómica, devolvendo o documento excluído, utilize findOneAndDelete():

const old = collection.findOneAndDelete(
_mongo.filters().eq("name", "Tablet")
)

Para excluir todos os documentos da coleção, passe um filtro vazio com a ajuda de _mongo.valToDoc():

collection.deleteMany(_mongo.valToDoc(_val.map()))

Contar Documentos

Com countDocuments() obtém o número de documentos da coleção, opcionalmente com um filtro. Já estimatedDocumentCount() é mais rápido pois utiliza os metadados da coleção, mas sem filtro:

const total = collection.countDocuments()

const totalMain = collection.countDocuments(
_mongo.filters().eq("category", "computers")
)

const estimated = collection.estimatedDocumentCount()

Índices

Com a factory _mongo.indexes() cria índices para melhorar o desempenho das consultas. Para criar um índice basta passar as especificações ao createIndex():

collection.createIndex(
_mongo.indexes().ascending("quantity")
)

collection.createIndex(
_mongo.indexes().compoundIndex(
_mongo.indexes().descending("price"),
_mongo.indexes().ascending("quantity")
)
)

Pode indicar ao MongoDB qual o índice a utilizar numa consulta, com o hint():

const docs = collection.find().hint(
_mongo.valToDoc(
_val.map().set("quantity", 1)
)
).all()

Com min() e max() você limita a gama de valores do índice percorridos pela consulta:

const docs = collection.find()
.min(
_mongo.valToDoc(
_val.map().set("quantity", 10)
)
)
.max(
_mongo.valToDoc(
_val.map().set("quantity", 50)
)
)
.all()

Agregações

Para análises mais avançadas, o aggregate() permite executar um pipeline de agregação, onde cada etapa é definida com a factory _mongo.aggregates() e os acumuladores com _mongo.accumulators().

Por exemplo, para agrupar os produtos pela categoria e somar o preço de cada grupo:

const docs = collection.aggregate(
_mongo.aggregates().match(
_mongo.filters().eq("category", "computers")
),
_mongo.aggregates().group(
"$category",
_mongo.accumulators().sum("total", "$price")
),
_mongo.aggregates().sort(
_mongo.sorts().descending("total")
)
).all()

As etapas de agregação disponíveis são as seguintes:

EtapaDescrição
match(filtro)Seleciona apenas os documentos que correspondem ao filtro.
group("$campo", acumuladores)Agrupa os documentos pelo campo e aplica os acumuladores.
project(projeção)Adiciona, remove ou altera campos nos documentos.
sort(ordenação)Ordena os documentos.
limit(n)Limita aos primeiros n documentos.
skip(n)Salta os primeiros n documentos.
count()Conta o número de documentos.
lookup("coleção", "campoLocal", "campoEstrangeiro", "como")Executa um join com outra coleção.

E os acumuladores mais utilizados:

AcumuladorDescrição
sum("campo", "$expressão")Soma dos valores.
avg("campo", "$expressão")Média dos valores.
min("campo", "$expressão")Valor mínimo.
max("campo", "$expressão")Valor máximo.
first("campo", "$expressão")Valor do primeiro documento.
last("campo", "$expressão")Valor do último documento.
top("campo", ordenação, "$expressão")Valor do topo segundo a ordenação.
bottom("campo", ordenação, "$expressão")Valor de baixo segundo a ordenação.

Nos acumuladores, a expressão com prefixo $ refere-se ao campo de um documento de entrada, por exemplo "$price".

Conversão entre Values e BSON

Por vezes é necessário converter entre os objetos _val do Netuno e documentos BSON do MongoDB:

const doc = _mongo.valToDoc(
_val.map().set("name", "Laptop")
)

const values = _mongo.docToVal(doc)

Operações com Opções

As factories *Options() do recurso _mongo devolvem os objetos de opções padrão do driver do MongoDB, e você pode encadear os seus métodos diretamente. Por exemplo, upsert(true) insere o documento quando o filtro não corresponde a nenhum documento existente:

const old = collection.findOneAndUpdate(
_mongo.filters().eq("name", "Monitor"),
_mongo.updates().set("quantity", 8),
_mongo.findOneAndUpdateOptions().upsert(true)
)

Quando o documento é inserido, o findOneAndUpdate() devolve null.

As factories de opções disponíveis são: insertOneOptions(), insertManyOptions(), updateOptions(), findOneAndUpdateOptions(), replaceOptions(), findOneAndReplaceOptions(), deleteOptions(), findOneAndDeleteOptions(), countOptions(), estimatedDocumentCountOptions(), dropCollectionOptions() e textSearchOptions().

Fechar a Conexão

Ao terminar, é uma boa prática fechar a conexão com o MongoDB:

_mongo.close()

Conclusão

Com o recurso _mongo o Netuno oferece uma abstração low-code e poliglota sobre o cliente oficial do MongoDB para Java, permitindo trabalhar com documentos, filtros, ordenações, projeções, atualizações, índices e agregações em JavaScript, Python, Ruby, Kotlin e Groovy.

Para mais detalhes sobre todos os métodos disponíveis, consulte a documentação do recurso _mongo e da classe MongoCollection.

Bom desenvolvimento!