Data

4 jan, 2018

MongoDB para iniciantes em NoSQL – Parte 05

Publicidade

E chegamos ao quinto artigo da minha série sobre MongoDB para iniciantes em NoSQL. Veja o começo desta série neste link.

Neste quinto artigo, tratarei de como manipular documentos que possuam subdocumentos e campos multivalorados, usando como base o banco de blog que modelamos no artigo anterior.

Boa parte do conteúdo deste artigo se encontra na segunda metade do vídeo abaixo que gravei pra Umbler. Além da leitura, recomendo a visualização para ver como tudo fica na prática:

CRUD com Subdocumentos

No segundo artigo desta série, nós vimos como buscar, inserir, atualizar e excluir documentos em coleções MongoDB. No entanto, sempre com documentos planos, sem níveis, a forma mais básica de armazenar dados.

Mas e quando temos um subdocumento dentro de outro documento, assim como o autor dentro de um artigo de blog?

{
   _id: ObjectId("123abc"),
   titulo: "Artigo 1",
   autor: {
      _id: ObjectId("456def"),
      nome: "Luiz"
   }
   tags: ["NodeJs", "MongoDB"]
}

Vamos começar pelo C do CRUD, que no caso do MongoDB é representado pelo método insert. O comando abaixo insere um novo artigo incluindo o subdocumento ‘autor’:

> db.artigos.insert({ titulo: "Artigo 1", autor: { _id: ObjectId("456def"), nome: "Luiz" }, tags: null })

Note que eu não passo o _id do artigo, pois ele é autoincremental e controlado pelo próprio MongoDB. Já no caso do autor, ele pertence à outra coleção, a de autores, e o _id que está junto dele deve ser o mesmo do autor original na sua referida coleção. Imagina-se que em uma aplicação que salve um artigo, que a informação do autor do artigo esteja em sessão ou em um componente de tela para ser passada ao MongoDB corretamente.

Falando do R do CRUD, o find no MongoDB, podemos facilmente usar campos de subdocumentos como filtros em nossas consultas, como segue:

> db.artigos.find({"autor.nome": "Luiz"})

Essa consulta retorna todos os artigos cujo nome do autor seja literalmente Luiz. Qualquer filtro existente pode ser usado aqui, sobre qualquer campo do subdocumento autor, mas atenção à forma que referenciei o campo, usando o nome do subdocumento, seguido de um ‘.’, e depois o nome do campo. Repare também que neste caso o uso de aspas ao redor da expressão é obrigatório.

Seguindo com o U do CRUD, vale tudo o que vimos até agora. Para substituir documentos que possuam subdocumentos usando o comando update, você tem de passá-los também, para não se perderem na atualização, como abaixo:

> db.artigos.update({_id: ObjectId("456def")},{ titulo: "Artigo 1", autor: { nome: "Luiz" }, tags: null })

Se for usar um campo de um subdocumento no filtro do update, valem as mesmas regras do filtro do find que expliquei anteriormente. O mesmo vale caso queira aplicar algum update operator em um campo de um subdocumento. Quer um exemplo prático?

Na modelagem de blog que fizemos no artigo anterior, replicamos o nome e _id do autor em todos os artigos escritos por ele. Mas o que acontece caso o nome do autor seja alterado no documento original dele, que fica na coleção de autores?

Teremos de replicar esta alteração em todos os artigos que tenham sido escritos por aquele autor, como abaixo. Neste exemplo, suponha que o autor Luiz teve o nome alterado para Luiz Fernando, então, temos de atualizar todos os posts escritos por ele. Como é somente esta pequena informação que mudou, usaremos o update-operator $set, para não ter de sobrescrever os documentos inteiros.

> db.artigos.update({"autor.nome":"Luiz"},{$set: {"autor.nome": "Luiz Fernando"}})

Para um update mais preciso, eu poderia substituir o filtro autor.nome por autor._id, considerando que nomes de autores podem ser repetir em um mesmo blog.

Com os update-operators $set, $unset e $rename é possível manipular os campos dos subdocumentos também, da mesma forma que faria com o documento principal, apenas usando a notação “subdocumento.campo”.

Finalizando o CRUD com subdocumentos, o D de delete é realizado usando as mesmas regras de filtro do find, caso queira excluir todos os documentos que possuam um valor específico em um campo de um subdocumento. Sem mistério algum.

CRUD com campos multivalorados

Outra preocupação é com a manipulação de elementos em campos multi-valorados, algo inexistente em bancos relacionais que sempre assumem relações 1-N ou N-N nestes casos. Salvo gambiarras que já vi por aí de salvar strings CSV ou JSON em coluna de registro e outras loucuras que sempre acabam gerando dor de cabeça, pois não são pesquisáveis.

Começando pelo C do CRUD, o insert do MongoDB funciona de maneira muito óbvia para campos multivalorados: apenas passe null para nenhum valor ou o array (entre colchetes) de valores iniciais daquele elemento (sim, o MongoDB permite que depois você adicione ou remova elementos).

Se você procurar no exemplo anterior de insert, verá que passei null no campo multivalorado de tags do artigo. A outra opção, passando valores iniciais, segue abaixo:

> db.artigos.insert({ titulo: "Artigo 1", autor: { nome: "Luiz" }, tags: ["NodeJs", "MongoDB"] })

Neste caso, o campo multivalorado tags é um array de strings. Caso deseje, você pode inserir um documento que possua campos multivalorados de documentos também, como no caso das categorias que modelamos no artigo anterior:

> db.artigos.insert({ titulo: "Artigo 1", autor: { nome: "Luiz" }, tags: ["NodeJs", "MongoDB"], categorias: [{_id:ObjectId("abc123"), nome: "Desenvolvimento"}] })

Mas é quando entramos no R do CRUD com campos multivalorados em MongoDB que começamos a explorar um novo universo de possibilidades e novos filter-operators da função find.

Considere que temos três artigos salvos na nossa base MongoDB (omitirei os campos que não nos importam no momento):

{
   _id: ObjectId("123abc"),
   titulo: "Artigo 1",
   tags: ["NodeJs", "MongoDB"],
   categorias: [{_id: 1, nome: "Desenvolvimento"}, {_id:2, nome: "Banco de Dados"}]
},
{
   _id: ObjectId("456def"),
   titulo: "Artigo 2",
   tags: ["NodeJs"],
   categorias: [{_id: 1, nome: "Desenvolvimento"}]
},
{
   _id: ObjectId("789ghi"),
   titulo: "Artigo 3",
   tags: ["MongoDB"],
   categorias: [{_id:2, nome: "Banco de Dados"}]
}

Para fazer buscas usando campos multivalorados como filtro é muito simples: você deve usar os operadores de filtro $all e $in. Exemplo de consulta por todos os artigos que possuam todas (ALL) as seguintes tags NodeJs e MongoDB:

> db.Artigo.find({tags: {$all: ["NodeJs", "MongoDB"]}});

O operador $all vai garantir que só sejam retornados artigos que possuam no mínimo as duas tags informadas (somente o Artigo 1), equivalente à consulta SQL abaixo:

SELECT Artigo.* FROM Artigo
INNER JOIN Tag ON Tag.IDArtigo = Artigo.ID
WHERE Tag.Nome = 'NodeJs'
INTERSECT
SELECT Artigo.* FROM Artigo
INNER JOIN Tag ON Tag.IDArtigo = Artigo.ID
WHERE Tag.Nome = 'MongoDB'

Agora um exemplo de consulta por todos os artigos que possuam uma (IN) das seguintes tags NodeJs ou MongoDB:

> db.Artigo.find({tags: {$in: ["NodeJs", "MongoDB"]}});

Neste caso, todos os três artigos serão retornados, assim como no equivalente SQL abaixo (coloquei reticências por preguiça de preencher todas as colunas):

SELECT Artigo.ID, Artigo.Titulo ... FROM Artigo
INNER JOIN Tag ON Tag.IDArtigo = Artigo.ID
WHERE Tag.Nome IN ("NodeJs", "MongoDB")
GROUP BY Artigo.ID, Artigo.Titulo ...

Mas agora se quisermos apenas os artigos que possuam ao menos uma categoria (o campo de categorias é multivalorado, lembra?) com o nome Desenvolvimento? Quando aplicamos um filtro sobre um campo que é multivalorado o MongoDB entende que podemos passar um novo filtro a seguir, que será aplicado aos campos do subdocumento. Por exemplo:

> db.artigos.find({categorias: {nome:"Desenvolvimento"}})

Vai retornar todos os documentos que tenham ao menos uma categoria cujo nome seja ‘Desenvolvimento’. Também poderia usar filter-operators se fosse necessário como $gte, $regex, etc.

Avançando para o U do CRUD, no update de campos multivalorados também temos diversas opções a serem exploradas, pois é no update que adicionamos e removemos elementos do nosso array, quando quisermos fazê-lo. Para tais tarefas deve-se usar os operadores $pull (remove o elemento do array) e $push (insere o elemento no array), sempre dentro de um comando de update, como segundo parâmetro (update-operator). Ex:

//remove a string tag1 do array tags
> db.artigos.updateOne({_id:1}, {$pull: {tags: "tag1"}})

//adiciona a string tag1 no array tags
> db.artigos.updateOne({_id:1}, {$push: {tags: "tag1"}})

Pode parecer um pouco estranho no início, mas na verdade é muito mais simples, pois é a mesma ideia que já fazemos há anos com coleções em linguagens de programação, onde geralmente temos métodos como add e remove ou assemelhados.

Finalizando, o D do CRUD em campos multivalorados funciona da mesma forma que o find, considerando que no deleteOne e deleteMany do MongoDB também passamos um filtro por parâmetro.