MongoDB_ Realizando Consultas

38 

Loading....

Loading....

Loading....

Loading....

Loading....

Texto

(1)

www.devmedia.com.br [versão para impressão]

Link original: http://www.devmedia.com.br/articles/viewcomp.asp?comp=27791

Artigo do tipo Tutorial

Recursos especiais neste artigo: Conteúdo sobre boas práticas.

Desvendando as consultas ao MongoDB

Este artigo aborda formas de consulta a dados tanto pelo Mongo Shell quando pela

linguagem Java. Neste contexto, são apresentadas consultas dos tipos simples e avançadas, o uso do framework de agregação e também da classe QueryBuilder. Não menos

importante, o artigo aborda ainda formas de conhecer o desempenho individual de cada consulta ao banco, assim como o uso de índices para melhorar o desempenho.

Em que situação o tema é útil

Este tema é útil para quem deseja começar a utilizar o banco de dados MongoDB e está interessado em informações sobre desempenho e formas de desenvolver suas consultas.

Atualmente os bancos de dados NoSQL (Not Only SQL) passaram a estar em evidência entre aplicações de grande porte e grandes empresas que utilizam de algum modo o

ambiente web, estão aderindo com muito entusiasmo a este novo modelo. Esta escolha está sendo feita por consequência de alguns fatores positivos oferecidos pelas soluções NoSQL em relação aos bancos relacionais. Dentre estes fatores temos uma maior velocidade na pesquisa dos dados, menor espaço físico em disco para armazenamento e fácil

(2)

escalabilidade, o que permite distribuir o banco de dados em várias máquinas fazendo uso do particionamento de dados, também conhecido como Sharding.

Em busca de fatores positivos como estes, grandes empresas já aderiram ao NoSQL, como a Google, Facebook, Twitter, Amazon.com, MTV Networks, Disney IMG, Cisco, entre outras. No Brasil, há algum tempo, em um evento sobre o banco de dados NoSQL MongoDB, o especialista Franklin Amorim da Globo.com revelou que o MongoDB foi o banco de dados escolhido para o jogo online CartolaFC. Atualmente essa é a maior aplicação da Globo.com, com mais de dois milhões de usuários cadastrados e com um pico de aproximadamente 90 milhões de páginas visualizadas em Junho de 2011. Normalmente, em seus projetos, a empresa usa como base de dados relacional o MySQL e a equipe da Globo.com revelou que encontrou algumas vantagens no uso do MongoDB sobre o MySQL. Entre elas, são citadas: a velocidade superior (2x mais rápida que o MySQL), o acesso mais natural aos dados e a possibilidade de escalar a escrita de dados com Sharding (sistema de compartilhamento do MongoDB) – leia mais sobre isso no endereço indicado na seção Links.

O MongoDB é um banco de dados livre, desenvolvido pela empresa Norte Americana 10gen. O banco foi desenvolvido sobre o conceito de documentos e coleções, onde cada coleção armazena vários documentos. Nessa arquitetura poderíamos tentar relacionar uma coleção a uma tabela e um documento a uma linha da tabela. Mas existem algumas diferenças bem significativas entre elas como, por exemplo, o MongoDB não possui relacionamento entre coleções, como existe entre tabelas de um banco relacional; os documentos são

considerados dinâmicos, ou seja, você pode ter um documento com cinco campos ao mesmo tempo em que há outros com três ou seis campos, por exemplo; ao invés de ter um relacionamento entre tabelas, é possível usar o conceito de documentos embutidos, ou seja, um documento dentro do outro.

Os documentos no MongoDB seguem o padrão JSON (JavaScript Object Notation), e após serem persistidos são transformados em BSON, um tipo de dado binário e serializado próprio do MongoDB. Com o formato dos documentos em JSON, a manipulação dos dados é realizada com base em chave (key) e valor (value), o que torna possível o dinamismo entre documentos. Se, por exemplo, em uma coleção qualquer, um novo documento a ser inserido conter entre suas chaves uma chave de valor nulo ou vazio, não é preciso inserir esta chave. Entretanto, em um momento futuro, quando existir um valor para tal chave, basta alterar o documento inserindo a chave e o valor e apenas este documento irá sofrer as alterações. Caso novas chaves sejam necessárias, em virtude da necessidade de armazenar alguma nova informação, elas podem ser inseridas nos documentos conforme cada documento sofrer alguma alteração como, por exemplo, um update. Já em um banco de dados relacional, você não teria essa facilidade, e precisaria criar uma instrução SQL de alteração da tabela para inserir uma nova coluna. Caso a nova coluna não tenha qualquer valor em algumas linhas, ela receberia o valor “null” nestas linhas. Este espaço na coluna ocupado pelo “null” seria mantido pelo SGDB para um futuro dado, o que acaba consumindo espaço físico em disco, e se este dado nunca for inserido, então se perde muito espaço em

(3)

disco armazenando apenas o valor “null”.

No entanto o objetivo do artigo não será salvar documentos ou testar o espaço gasto em disco para armazená-los, e sim, explorar diretamente as queries, ou métodos de consultas a dados a partir de código Java e também pelo Shell do MongoDB. Dentre os métodos de consulta temos as consultas simples (Simple Queries), consultas avançadas (Advanced Queries), o framework de agregação (Aggregation Framework) e consultas do tipo QueryBuilder.

Todos esses tipos consultas serão apresentados no artigo por meio de exemplos práticos em Java e também no mongo Shell. Além disso, será demonstrado como alcançar um melhor desempenho nas consultas com a adição de índices na coleção de documentos e veremos também as ferramentas oferecidas pelo MongoDB para avaliação de desempenho das queries. Uma coleção com inúmeros documentos será disponibilizada ao leitor para download, a qual será usada como exemplo para os testes das queries, juntamente com o código fonte Java utilizado no projeto.

A coleção Users contém os dados que serão usados como exemplo, na execução das queries, durante todo o artigo. Para obter esta coleção é necessário acessar a seção de downloads da Java Magazine, baixar o conteúdo (arquivo contendo a coleção) e fazer a importação para o banco de dados MongoDB. Os passos para realizar a importação serão demonstrados na seção “Dependências do Projeto”.

Antes de importar esta coleção, no entanto, é importante conhecer a estrutura dos

documentos que ela armazena. Para isso, observe a Listagem 1. Note que o documento é idêntico a qualquer documento JSON, onde se tem um nome para a chave (key) e um determinado valor (value) para esta chave. Neste exemplo é possível observar que a chave _id possui o valor 8, representando o identificador do documento, equivalente, por exemplo, à chave primária (PK) de uma tabela em bancos de dados relacionais.

O identificador no MongoDB, conforme definido por padrão, será sempre precedido por um underline. No documento da coleção Users, existem outros campos além do _id, como as chaves name e age. A chave info é o que chamamos de documento interno (embedded document), ou sub-documento. Este tipo de documento normalmente é composto por pelo menos uma nova chave interna, e pode substituir a necessidade de se criar uma nova coleção para armazenar as informações contidas nele.

Na coleção Users, info possui as chaves internas hometown e job. Outra forma de

armazenar dados em um documento Mongo é usando arrays, como exemplificado na chave preferences. Esta chave armazena valores sobre as preferências do usuário, como se fossem tags encontradas em sistemas de blog que identificam os principais assuntos de uma postagem.

(4)

Os documentos da coleção Users foram gerados através de um processo randômico, podendo assim possuir documentos contendo nenhuma ou varias preferências para cada usuário. Isto foi feito propositalmente para que o leitor veja que um documento Mongo não precisa ter a estrutura idêntica a todos os outros documentos da coleção, como acontece, por exemplo, em tabelas de bancos de dados relacionais, onde todas as linhas de uma tabela sempre terão todas as colunas.

Listagem 1. Exemplo de um documento da coleção Users.

{

"_id" : 8,

"name" : "Rodrigo Miranda Radel", "age" : 70,

"info" : {

"hometown" : "Joao Pessoa", "job" : "Comediante" }, "preferences" : [ "Automobilismo", "Basquete", "Robotica" ] }

Agora observe a Listagem 2. Pode-se notar que o documento apresentado não possui a chave preferences, já que o usuário não tem nenhuma preferência adicionada. Esta é uma grande vantagem encontrada no MongoDB que faz reduzir o consumo físico de memória no disco rígido. Se um campo não é utilizado, ao invés de ter este campo com um valor vazio ou mesmo nulo, ocupando um espaço sem uma informação significante, é mais vantajoso não adicioná-lo no documento. Pode ser que para um banco de dados com dezenas, ou mesmo centenas de registros esta diferença nem seja percebida, mas quando trabalhamos com milhares ou milhões de dados, a soma de vários campos sem informação alguma pode acabar resultando em muito espaço físico desperdiçado. E como um documento Mongo é considerado dinâmico, a qualquer momento é possível realizar uma alteração neste

documento e inserir as preferências do usuário. Tenha em mente que qualquer chave de um documento da coleção Users pode ser omitido, exceto a chave _id, por possuir o mesmo papel de uma chave primária em bancos relacionais.

Listagem 2. Documento sem a chave preferences.

{

"_id" : 16,

"name" : "Daniela Pinho Milito", "age" : 58,

"info" : {

"hometown" : "Alagoinhas", "job" : "Carpinteiro"

(5)

} }

Por causa das dependências do projeto que será construído neste artigo, serão necessários alguns downloads e também a instalação do banco de dados MongoDB, como também a importação da coleção Users. Para realizar o download do banco de dados, acesse o endereço referente na seção Links e faça o download da versão 2.2.2, conforme o seu sistema operacional. Faça também o download do mongo-java-driver.jar, a API fornecida pelo MongoDB para acesso Java ao banco de dados, o qual deverá ser adicionado a sua aplicação. A URL para download do driver versão 2.10.1, usada no artigo, também pode ser encontrada na seção Links.

A instalação do banco de dados é muito simples, bastando realizar os seguintes passos (exemplo em Windows):

· Faça a descompactação do arquivo baixado;

· Copie o conteúdo extraído para o diretório c:\;

· Para facilitar a execução das instruções futuras na linha de comando, altere o nome da pasta raiz descompactada para “mongo”;

· Entre no diretório c:\mongo\bin e copie todos os arquivos existentes. Cole estes arquivos em c:\mongo;

· Crie um novo diretório chamado “data” dentro do diretório c:\mongo. Este novo diretório será onde o MongoDB irá armazenar o banco de dados.

Chegou o momento da importação da coleção Users para o MongoDB. Para isso, descompacte o arquivo users.rar e copie o seu conteúdo (users.json) para o diretório c:\mongo. Em seguida abra uma janela do console do seu sistema operacional e navegue até o diretório c:\mongo. Antes da importação da coleção, no entanto, devemos inicializar o MongoDB executando o seguinte comando:

c:\mongo> mongod --dbpath data

O comando mongod é o responsável por inicializar o MongoDB, e o parâmetro ––dbpath indica ao MongoDB onde está armazenado o banco de dados, que neste caso é no diretório data. Se a inicialização ocorreu com sucesso, você terá um retorno no console semelhante ao exibido na Listagem 3.

(6)

Listagem 3. Log de inicialização.

Thu Jan 03 16:52:19 [initandlisten] MongoDB starting : pid=4352 port=27017 dbpath=data 64-bit host=MarcioBallem-PC

Thu Jan 03 16:52:19 [initandlisten] db version v2.2.0, pdfile version 4.5 Thu Jan 03 16:52:19 [initandlisten] git version:

f5e83eae9cfbec7fb7a071321928f00d1b0c5207

Thu Jan 03 16:52:19 [initandlisten] build info: windows sys.getwindowsversion

(major=6, minor=1, build=7601, platform=2, service_pack='Service Pack 1')

BOOST_LIB_VERSION=1_49

Thu Jan 03 16:52:19 [initandlisten] options: { dbpath: "data" } Thu Jan 03 16:52:19 [initandlisten] journal dir=data/journal Thu Jan 03 16:52:19 [initandlisten] recover :

no journal files present, no recovery needed Thu Jan 03 16:52:22 [initandlisten] waiting for connections on port 27017

Thu Jan 03 16:52:22 [websvr] admin web console waiting for connections on port 28017

Para mais informações sobre a instalação do MongoDB, veja a seção Links. Com o MongoDB rodando, devemos agora importar a coleção Users. Portanto, abra uma nova janela do console e digite o seguinte comando:

c:\mongo> mongoimport -d devmedia -c users < users.json

O comando mongoimport representa a instrução de importação e o parâmetro –d define a base de dados, que no caso será chamada de devmedia. O parâmetro –c indica o nome da coleção que será importada. Já a instrução < user.json faz referência ao arquivo que será importado. Feito isso, deve ser exibido no console um retorno semelhante a este:

connected to: 127.0.0.1

Thu Jan 03 17:10:36 19000 6333/second

Thu Jan 03 17:10:39 51300 8550/second

Thu Jan 03 17:10:42 83800 9311/second

Thu Jan 03 17:10:43 imported 100000 objects

Note que o número de objetos importados foi de cem mil, ou seja, a coleção Users é formada por cem mil documentos. A coleção e o banco de dados são criados

automaticamente durante a execução da instrução de importação.

Veja a seguir alguns comandos necessários para acessar a coleção importada, como também uma simples consulta para retornar um documento qualquer:

(7)

MongoDB shell version: 2.2.0

connecting to: test

> use devmedia

switched to db devmedia

> db.users.findOne()

O comando mongo inicializa uma instância do Mongo Shell, que é a forma de interação entre usuários e o banco de dados. O comando use devmedia informa que o usuário irá acessar o banco de dados devmedia. Já dentro da base de dados devmedia, todos os comandos sobre a coleção Users deverão ser precedidos pela instrução db.users, seguidos então do comando a ser executado. Neste caso usamos o comando findOne(), que é um método responsável por retornar o primeiro registro encontrado na base de dados. O resultado deverá ser idêntico ao documento exibido na Listagem 4. Para conhecer outros comandos, utilize a instrução db.users.help().

Os próximos comandos executados no Mongo Shell deverão ser efetuados no banco devmedia, conforme ocorreu com o comando db.users.findOne().

Listagem 4. Documento retornado após a execução do comando findOne().

{

"_id" : 1,

"name" : "Vicente Pereira Pirez", "age" : 48,

"info" : {

"hometown" : "Joao Pessoa", "job" : "Paleontologo" }, "preferences" : [ "Video Games" ] }

O passo inicial para o projeto Java que servirá como exemplo durante todo o artigo é criar as classes de entidades, as quais representarão os documentos da coleção Users. A classe User, apresentada na Listagem 5, representa o documento principal e possui todos os atributos referentes às chaves dos documentos da coleção. Note também que a variável de instância info se refere à classe Info, apresentada na Listagem 6, e representa o

embedded document do documento principal. Listagem 5. Código da classe User.

(8)

package br.com.devmedia.mongo.entity;

import java.io.Serializable; import java.util.Arrays;

public class User implements Serializable {

private String id; private String name; private int age;

private String[] preferences; private Info info;

// getters e setters omitidos

@Override

public String toString() { return "User{" +

"id='" + id + '\'' + ", name='" + name + '\'' + ", age=" + age +

", preferences=" + (preferences == null ? null : Arrays.asList(preferences)) +

", info=" + info + '}';

} }

Listagem 6. Código da classe Info.

package br.com.devmedia.mongo.entity;

import java.io.Serializable;

public class Info implements Serializable {

private String hometown; private String job;

// getters e setters omitidos

@Override

public String toString() { return "Info{" + "hometown='" + hometown + '\'' + ", job='" + job + '\'' + '}'; } }

(9)

Uma prática comum em Java para acesso ao banco de dados é a classe de conexão, necessária para a interação entre a aplicação Java e o banco de dados. A documentação MongoDB sugere que esta classe seja implementada com base no padrão Singleton. Este padrão de projeto tem como objetivo garantir que apenas uma única instância da classe esteja disponível durante todo o ciclo de vida da aplicação. Para que isso aconteça, uma das formas é declarar o construtor da classe como privado, evitando assim que alguma outra classe tenha acesso a ele. Ademais, uma variável estática deve ser criada para guardar a instância da classe, que será criada dentro da própria classe, e um método estático irá fornecer um único ponto de acesso a esta instância sempre que for requisitado. Veja na Listagem 7 a classe MongoConnection, implementada com base no padrão Singleton.

Listagem 7. Código da classe MongoConnection.

package br.com.devmedia.mongo.dao; import com.mongodb.DB; import com.mongodb.MongoClient; import java.net.UnknownHostException;

public class MongoConnection {

private static final String HOST = "localhost"; private static final int PORT = 27017;

private static final String DB_NAME = "devmedia";

private static MongoConnection uniqInstance; private static int mongoInstance = 1;

private MongoClient mongo; private DB db; private MongoConnection() { //construtor privado }

//garante sempre uma única instância da classe

public static synchronized MongoConnection getInstance() { if (uniqInstance == null) {

uniqInstance = new MongoConnection(); }

return uniqInstance; }

//garante a existência de um único objeto mongo public DB getDB() {

(10)

try {

mongo = new MongoClient(HOST, PORT); db = mongo.getDB(DB_NAME);

System.out.println("Mongo instance equals :> " + mongoInstance++); } catch (UnknownHostException e) { e.printStackTrace(); } } return db; } }

O driver do MongoDB é thread-safe. Desta forma, durante o ciclo de vida de uma aplicação é necessária apenas uma instância da classe de conexão, abstraindo do programador a necessidade de desenvolver métodos responsáveis por abrir e fechar objetos de conexão, como é feito, por exemplo, em aplicações que utilizam conexões JDBC. A classe responsável por esse controle é a com.mongodb.Mongo, utilizada em versões anteriores ao driver 2.10.0 como classe de conexão. A partir da versão 2.10.0 os desenvolvedores da 10gen

implementaram a nova classe com.mongodb.MongoClient, que na verdade é subclasse de com.mongodb.Mongo – saiba mais sobre MongoClient na seção Links.

Após a conexão ser efetuada, o Mongo retornará um objeto do tipo com.mongodb.DB. A partir deste objeto o desenvolvedor terá acesso a todos os métodos fornecidos pela API do MongoDB. Entre esses métodos estão alguns como o save(), para salvar um novo

documento; update(), para alterar um documento já existente na coleção; remove(), para excluir um documento da coleção; find(), para localizar um documento ou uma lista deles; e findOne(), para recuperar apenas um documento.

Algo muito importante ao trabalhar com o MongoDB são as classes de conversão. Este tipo de classe será responsável por converter um documento Mongo em um objeto Java e vice-versa. Infelizmente a API MongoDB não fornece este tipo de classe e o trabalho de

implementação fica por conta do programador. Apesar disso, não se assuste, pois a implementação é muito simples, como se pode verificar nas Listagens 8 e 9.

Listagem 8. Código da classe de conversão InfoConverter.

package br.com.devmedia.mongo.converter; import br.com.devmedia.mongo.entity.Info; import br.com.devmedia.mongo.entity.User; import com.mongodb.BasicDBList; import com.mongodb.DBObject;

(11)

public class InfoConverter {

public static Info converterToInfo(DBObject dbo) { Info info = new Info();

info.setHometown((String) dbo.get("hometown")); info.setJob((String) dbo.get("job")); return info; } }

Vamos começar analisando o método converterToInfo() da Listagem 8, onde temos a conversão de um objeto Mongo para um objeto Java. Note que este método tem como parâmetro um objeto do tipo com.mongodb.DBObject. É por este objeto que temos acesso ao documento retornado por uma consulta ao banco de dados. O objeto armazena o

documento em formato JSON e por este motivo precisa ser convertido em um objeto do tipo Info. Para recuperar os valores do documento, se usa o método get(), o qual retorna um objeto do tipo java.lang.Object. O get() recebe como parâmetro uma String com o nome da chave referente ao valor que será recuperado no documento contido no objeto DBObject. Como o Java é uma linguagem fortemente tipada, e o método get() retorna um objeto do tipo Object, devemos transformar esse retorno no tipo esperado pelo método set() da classe Info. Para isso, basta usar o cast fornecido pelo Java.

Na Listagem 9, o método converterToUser() é responsável por converter o retorno do banco de dados em um objeto do tipo User. O sistema de conversão é muito semelhante ao usado pelo método converterToInfo(), mas possui duas importantes diferenças. Observe que o método setInfo() recebe como parâmetro o retorno do método converterToInfo(). Isto é devido à necessidade de converter o retorno do documento interno em um objeto Info e essa conversão é de responsabilidade da classe InfoConverter. Por último, o método setPreferences() espera como parâmetro um array de strings que é também o tipo de retorno da consulta. Para recuperar um array, contido em um DBObject, se deve usar o objeto com.mongodb.BasicDBList. Como nem todos os documentos da coleção possuem uma lista de preferências, é aconselhável um teste para verificar se o objeto BasicDBList possui valor nulo. Caso seu valor não seja nulo, basta transformar a lista de preferências em um array de strings e o inserir no objeto User através do método setPreferences().

Listagem 9. Código da classe de conversão UserConverter.

package br.com.devmedia.mongo.converter; import br.com.devmedia.mongo.entity.User; import com.mongodb.BasicDBList; import com.mongodb.DBObject;

(12)

public static User converterToUser(DBObject dbo) { User user = new User();

user.setId(dbo.get("_id").toString()); user.setName((String) dbo.get("name")); user.setAge((Integer) dbo.get("age"));

user.setInfo( InfoConverter.converterToInfo((DBObject) dbo.get("info") ) );

BasicDBList dbList = (BasicDBList) dbo.get("preferences"); if (dbList != null) {

user.setPreferences( dbList.toArray(new String[dbList.size()] ) ); }

return user; }

}

O cast, ou a conversão de objetos em Java, acontece quando atribuímos um objeto de uma classe superior (superclasse) a um objeto de uma classe inferior (subclasse). Para isso acontecer devemos fazer um casting para indicar qual objeto de subclasse queremos criar.

É uma boa prática no desenvolvimento de aplicações o uso de padrões de projeto. Sendo assim, no projeto exemplo vamos usar o padrão Data Access Object (DAO) na camada de persistência. Neste caso em especial, como o artigo tem foco apenas em métodos de pesquisa, não será implementado nenhum outro tipo de método a não ser os de consulta. O DAO é um importante padrão de projetos que tem como objetivo isolar as classes de acesso a banco de dados das demais classes do sistema. Com este isolamento, o código fica mais organizado, facilitando tanto a manutenção da aplicação como também fornece a

possibilidade de reuso das mesmas classes em diversos projetos distintos. Uma das regras do padrão DAO é fornecer acesso aos métodos do CRUD através de uma interface e codificar estes métodos em uma classe concreta. Sendo assim, na Listagem 10 temos a interface IUserDao, com a assinatura dos métodos que serão codificados na classe concreta UserDao, exibida na Listagem 11. Embora a interface tenha seis assinaturas de métodos, em um primeiro momento na classe UserDao será apresentado apenas o método

findUsers(), e mais a frente no artigo, conforme se tornar necessário, os demais métodos serão abordados.

O método findUsers() é muito simples, como pode ser verificado na Listagem 11. Vale observar que ele possui um parâmetro do tipo com.mongodb.DBObject. Esta é uma importante interface presente na API MongoDB, que é implementada por pelo menos duas classes concretas que serão muito utilizadas em Java para a interação com o banco de dados, a com.mongodb.BasicDBObject e com.mongodb.BasicDBList. O objeto

(13)

BasicDBObject possui uma estrutura similar a objetos java.util.Map, onde se trabalha com chave e valor. Já o objeto BasicDBList é similar ao java.util.List.

O parâmetro DBObject será responsável pelos critérios utilizados nas consultas

apresentadas. Seguindo a análise do método findUsers(), a primeira instrução representa uma simples inicialização da lista users, que será o retorno do método com os resultados localizados no banco. A instrução seguinte contém um objeto da classe

com.mongodb.DBCursor, o qual tem como objetivo armazenar o retorno do método find(), da API MongoDB, que será uma lista de documentos Mongo. Para manipular esta lista e transformá-la em um objeto nativo do Java é preciso realizar um while() e adicionar cada documento de DBCursor em uma posição da lista users. Feito isso, basta adicionar users como retorno do método findUsers() para ter acesso aos documentos armazenados na coleção.

Listagem 10. Código da interface IUserDao.

package br.com.devmedia.mongo.dao; import com.mongodb.DBObject; import java.io.Serializable; import java.util.Collection; import java.util.List;

public interface IUserDao<User extends Serializable> {

List<User> findUsers(DBObject keyValue);

Iterable<DBObject> showSumAndAvgByAge();

Iterable<DBObject> showSumAndAvgByAge(int age);

Iterable<DBObject> showCountByPreferences (Collection<String> preferences);

Iterable<DBObject> showCountUsersByHometown(String city);

List<User> queryBuilder(); }

Listagem 11. Código da classe concreta UserDao.

package br.com.devmedia.mongo.dao;

import br.com.devmedia.mongo.converter.UserConverter; import br.com.devmedia.mongo.entity.User;

(14)

import java.util.*;

public class UserDao implements IUserDao<User> {

// objeto de acesso aos métodos do driver mongodb private DBCollection db;

// objeto list para armazenar os resultados private List<User> users;

// construtor que inicializa o objeto db com uma instância da classe de conexão public UserDao() {

this.db = MongoConnection.getInstance().getDB().getCollection("users"); }

public List<User> findUsers(DBObject keyValue) {

users = new ArrayList<User>();

DBCursor cursor = db.find(keyValue); while (cursor.hasNext()) { users.add(UserConverter.converterToUser(cursor.next())); } return users; } }

Entre as operações do CRUD, o retrieve (recuperar) é a operação responsável pela consulta e retorno dos dados armazenados em banco. No MongoDB este tipo de ação foi nomeada como Read Operations (operações de leitura), conforme consta na documentação. As operações de leitura, ou consultas ao banco, serão apresentadas neste artigo de duas formas. Primeiro será demonstrada a consulta no próprio padrão Mongo, a qual

normalmente é utilizada para execução no Mongo Shell. Em seguida, será demonstrado como implementá-la em código Java. Grande parte das consultas em Java apresentadas neste artigo irá utilizar o método findUsers() – veja a Listagem 11. Ele será o responsável por realizar as consultas ao banco, porém, precisamos também especificar os critérios da consulta por parâmetro, e é nesta parte que uma consulta a documentos pode se tornar um pouco complicada. Por este motivo é interessante entender como a consulta é realizada no Mongo Shell, para então construí-la em Java.

(15)

Uma consulta é caracterizada como Simple Query por utilizar apenas chaves e valores. Por curiosidade, algumas consultas deste tipo não precisam nem mesmo de chave e valor, como é o caso do método findOne(), que retorna o primeiro resultado encontrado, e o método find(), que retorna todos os documentos de uma coleção. Veja na Listagem 12 uma Simple Query apresentada no padrão Mongo Shell. Esta consulta tem como objetivo retornar todos os documentos de usuários com idade igual a 30 anos. Veja que o método find() recebe como parâmetro a chave age e o valor 30. A chave sempre deverá estar separada do valor pelo operador dois pontos (:) e ambos devem ficar dentro de um par de chaves ({ key : value }).

Listagem 12. Consulta por idade – Mongo Shell.

> db.users.find( { "age" : 30 } );

Sabendo como criar esta consulta no Mongo Shell, fica mais fácil fazê-la também em Java. Observe na Listagem 13 o método findByAge(). Primeiro inicializamos o objeto dao, e em seguida é criado um objeto chamado query do tipo BasicDBObject. Os critérios da consulta são informados por meio do método put() de BasicDBObject. Note que a chave do

documento é adicionada como chave no objeto query e o valor do documento é informado como sendo o valor que representa tal chave. Seguindo o código, o próximo passo é adicionar o objeto query ao parâmetro esperado pelo método findUsers() da classe UserDao, e por fim, através de um for(), o resultado é impresso no console da IDE.

Listagem 13. Código da consulta por idade – Método Java findByAge().

private void findByAge() { dao = new UserDao();

BasicDBObject query = new BasicDBObject();

query.put("age", 30);

List<User> users = dao.findUsers(query);

for (User user : users) {

System.out.println(user.toString()); }

}

No padrão SQL existem alguns operadores, ou funções, bastante utilizadas em consultas como, por exemplo, soma, média, menor ou igual, maior que, máximo, mínimo, entre outras. Estes operadores existem também no MongoDB e quando utilizados as consultas são

(16)

chamadas de Advanced Queries. Algumas das consultas que serão apresentadas neste artigo farão uso destes operadores. Por exemplo, a próxima consulta usará os critérios de maior ou igual e menor ou igual para simular a função between do SQL. Contudo, antes disso, confira na Tabela 1 alguns dos operadores.

Tabela 1. Funções de alguns operadores MongoDB semelhantes ao SQL.

Agora que conhecemos alguns operadores Mongo, vamos utilizá-los. Na Listagem 14, por exemplo, a consulta deverá retornar documentos em que usuários tenham idades que sejam maior ou igual a 16 anos e menor ou igual a 17 anos. Note que, em relação à consulta anterior, esta nova consulta ficou levemente mais complicada. A chave da consulta continua sendo age, porém, no momento de selecionar o valor como critério, foi adicionado um par de chaves e, dentro deste par, inseridos os critérios de idade e os operadores de maior ou igual e menor ou igual. Sempre que for utilizar um operador, ele deve ser colocado dentro de um par de chaves, e se houver mais de um operador, eles devem ser separados por vírgula (,).

Listagem 14. Consulta entre idades – Mongo Shell.

> dB.users.find( { "age" : { "$gte" : 16 , "$lte" : 17} } )

E agora, como transpor esta consulta para código Java? Bom, se torna também um pouco mais trabalhoso, mas nem tanto assim, como demonstra na Listagem 15 o método findBetweenAges(). Ao analisá-lo, pode-se notar que foi adicionado o objeto between do

(17)

tipo BasicDBObject para receber os critérios de idade. Os critérios são inseridos no objeto através do método put(), onde a chave é a função e o valor representa a idade. No objeto query, também do tipo BasicDBObject, é adicionada a chave age e como valor, o objeto between contendo os critérios da pesquisa. Por fim, invocamos o método findUsers() passando como parâmetro o objeto query.

Listagem 15. Consulta entre idades – Método Java findBetweenAges().

private void findBetweenAges() { dao = new UserDao();

BasicDBObject between = new BasicDBObject(); between.put("$gte", 16);

between.put("$lte", 17);

BasicDBObject query = new BasicDBObject(); query.put("age", between);

List<User> users = dao.findUsers(query);

for (User user : users) {

System.out.println(user.toString()); }

}

E se além da idade fosse necessário adicionar outro critério como, por exemplo, algumas preferências do usuário? Essa consulta precisaria sofrer uma pequena modificação. Vejamos então como ficaria no Mongo Shell, conforme a Listagem 16.

A primeira parte da consulta, referente à idade, já sabemos como funciona. Mas agora existe um segundo critério, que são as preferências do usuário. Deste modo, será

adicionada a chave preferences, e como valor é inserido o operador $in e um array com dois tipos de preferências. Com isso, a consulta irá retornar todos os usuários com idade entre 16 e 17 anos e que tenham as preferências por “Series” e/ou “Motociclismo”. Caso o operador $in seja substituído pelo operador $all, o retorno da pesquisa traria apenas os usuários com preferências em “Series” e em “Motociclismo”.

Também é possível pesquisar em um array sem o uso dos operadores $in e $all, porém, desta forma, só haverá retorno quando a sequência de preferências for idêntica ao parâmetro, ou seja, [‘Series’, ‘Motociclismo’]. Caso o documento contenha ambos os parâmetros, mas não respeitando a sequência informada, como [‘Series’, ‘Movies’, ‘Motociclismo’] ou [‘Motociclismo’, ‘Series’], estes não serão retornados.

(18)

> db.users.find({

"age" : { "$gte" : 16 , "$lte" : 17 } ,

"preferences" : { "$in" : [ "Series" , "Motociclismo"] } })

Na Listagem 17, o código para Java desta consulta é apresentado pelo método

findBetweenAgesAndPreferences(). Em relação ao método da Listagem 15, existem duas diferenças. A primeira é constituída por dois novos objetos, o objeto array, do tipo

BasicDBList, para armazenar a lista de preferências, e o objeto in, do tipo BasicDBObject. Neste objeto, uma chave com o operador $in é adicionada e, como valor, ele recebe o objeto array. A segunda diferença é encontrada no objeto query, que além de ter adicionado a ele a chave age e seu valor, também é adicionada a chave preferences, que terá como valor o objeto in.

Listagem 17. Consulta entre idades e preferências – Método Java findBetweenAgesAndPreferences().

private void findBetweenAgesAndPreferences() { dao = new UserDao();

BasicDBObject between = new BasicDBObject(); between.put("$gte", 16);

between.put("$lte", 17);

BasicDBList array = new BasicDBList(); array.add("Series");

array.add("Motociclismo");

BasicDBObject in = new BasicDBObject("$in", array);

BasicDBObject query = new BasicDBObject(); query.put("age", between);

query.put("preferences", in);

List<User> users = dao.findUsers(query);

for (User user : users) {

System.out.println(user.toString()); }

}

Como as consultas avançadas oferecem diversos operadores, vamos analisar dois novos operadores não utilizados nas consultas anteriores, $and e $regex. O critério desta nova pesquisa será localizar usuários com um nome e uma idade específicos. Veja na Listagem

(19)

18 a consulta preparada para o Mongo Shell.

Quando é feito o uso do operador $and se deve colocar os critérios dentro de um array. Neste caso, na primeira posição do array teremos o nome do usuário e na segunda posição teremos a idade. Note também que no valor da chave name é usado o operador $regex, que neste exemplo irá localizar todos os documentos que tenham “Marcio” como parte do nome.

Listagem 18. Consulta entre nome e idade – Mongo Shell.

> db.users.find({ "$and" : [

{ "name" : { "$regex" : "Marcio"} } , { "age" : 25 }

] })

Agora, observe na Listagem 19 o método findUserByNameAndAge(), onde também são usados os operadores $and e $regex. Neste método temos o objeto regex, do tipo

BasicDBObject, que contém o critério de seleção por nome. Em seguida o objeto name, do tipo BasicDBObject, recebe a chave name, e seu valor será o objeto regex. Um novo objeto BasicDBObject é criado para armazenar o critério por idade. Como na consulta do Mongo Shell foi criado um array para adicionar os critérios do nome e da idade, em Java devemos proceder do mesmo modo. Portanto, o objeto andList, do tipo BasicDBList, foi adicionado ao código, e nele são inseridos os critérios name e age. Por fim, o objeto query recebe como chave o operador $and e como valor a lista de critérios andList.

Listagem 19. Consulta entre nome e idade – Método Java findUserByNameAndAge().

private void findUserByNameAndAge() { dao = new UserDao();

BasicDBObject regex = new BasicDBObject("$regex", "Marcio");

BasicDBObject name = new BasicDBObject("name", regex);

BasicDBObject age = new BasicDBObject("age", 25);

BasicDBList andList = new BasicDBList(); andList.add(name);

andList.add(age);

BasicDBObject query = new BasicDBObject(); query.put("$and", andList);

List<User> users = dao.findUsers(query);

for (User user : users) {

(20)

} }

Até o momento analisamos consultas em chaves simples, como name e age e também em preferences, que é um array. Nos documentos da coleção Users, temos ainda uma chave que representa um documento interno. Esta chave é a info, e possui duas chaves internas, hometown e job. Vamos então montar uma consulta que retorne todos os documentos de uma cidade específica, usando a chave interna hometown. Veja na Listagem 20 como proceder esta consulta.

A chave info, como já informado, representa o documento interno a ser pesquisado. Para selecionar uma chave interna deste documento, basta usar a instrução dot (ponto) seguida pela chave desejada e o valor, que neste exemplo conterá o nome da cidade.

Listagem 20. Consulta em documento interno – Mongo Shell.

> db.users.find( { "info.hometown" : "Porto Alegre"} )

Em Java esta consulta também é muito simples. Observe na Listagem 21 o método findInfo(). É necessário apenas criar um objeto do tipo DBObject, informar a chave com a instrução dot e, como valor, adicionar o nome da cidade requerida.

Listagem 21. Consulta em documento interno – Método Java findInfo().

private void findInfo() { dao = new UserDao();

DBObject query = new BasicDBObject("info.hometown", "Porto Alegre");

List<User> users = dao.findUsers(query);

for (User user : users) {

System.out.println(user.toString()); }

}

Com todo esse conteúdo analisado já é possível ter uma ideia bastante ampla sobre consultas em Mongo, sendo elas em modo Shell ou mesmo em código Java. Abordamos até aqui, além da Simple Queries, também as Advanced Queries, que fazem uso de operadores semelhantes aos encontrados no SQL. Na coleção Users, trabalhamos em diversas consultas que utilizaram todas as chaves dos documentos, e assim fomos capazes de realizar

consultas por chaves simples, mescladas com operadores e também consultas por array e documentos internos.

(21)

Um índice é uma estrutura de dados que permite localizar rapidamente documentos com base em valores armazenados em campos específicos. Fundamentalmente, os índices no MongoDB são semelhantes aos índices disponíveis em bancos de dados relacionais. Esta solução NoSQL suporta índices em qualquer campo ou subcampo contido em documentos dentro de uma coleção.

Veja a seguir algumas características básicas sobre índices no MongoDB:

· O MongoDB define os índices por coleção. Assim, cada coleção tem seus próprios índices;

· Você pode criar índices em um único campo ou em vários campos, usando um índice composto (compound index);

· Índices melhoram o desempenho da consulta, muitas vezes de forma formidável;

· Todos os índices do MongoDB utilizam uma estrutura de dados em árvore chamada B-Tree;

· Cada consulta usa apenas um índice. Se existir mais de um índice que possa ser empregado, o MongoDB possui um sistema que escolherá a melhor opção;

· Usando um bom índice é reduzido o número de documentos que o MongoDB precisa armazenar em memória, maximizando o desempenho do banco de dados.

Um fato importante em relação ao MongoDB está no momento de criar o banco de dados. Diferentemente de bancos relacionais, o MongoDB não trabalha de forma normalizada. Sendo assim, é aconselhado, ao criar uma coleção, pensar antes nos dados que serão recuperados do banco, para que a coleção não se torne complexa demais para realizar consultas sobre ela. Usando este conceito, suas coleções estarão mais fáceis de serem manuseadas, tanto por você quanto pelo próprio MongoDB.

No caso de índices, não é interessante criar um índice para chaves que não serão usadas em pesquisas, porque a grande quantidade de índices em uma coleção pode ocasionar um processo mais lento no momento de salvar dados no banco, já que para cada documento o Mongo terá que criar vários indexadores.

Na Ciência da Computação, uma árvore B é uma estrutura de dados projetada para funcionar especialmente em memória secundária, como um disco magnético ou outros dispositivos de armazenamento secundário. Dentre suas propriedades ela permite a inserção, remoção e busca de chaves numa complexidade de tempo logarítmico e, por esse motivo, é muito empregada em aplicações que necessitam manipular grandes quantidades de informação, tais como um banco de dados ou um sistema de arquivos.

(22)

Algo muito interessante fornecido pelo MongoDB são algumas ferramentas internas para análise do desempenho das queries. Por meio de uma destas ferramentas é possível criar um tipo de log que grava a execução de todas as consultas executadas no banco e a partir desse log o desenvolvedor pode verificar quais consultas precisam, por exemplo, ser indexadas ou ter um melhor índice. Este processo é conhecido como Profiling.

Existem três níveis de Profiling disponíveis, a saber:

· Nível 0 (nível padrão): profiler desativado;

· Nível 1: profiler ativado. Registra por padrão todas as consultas mais lentas que 100 milissegundos. É possível alterar esse tempo;

· Nível 2: profiler ativado. Registra todas as consultas, independentemente do tempo de resposta de cada uma.

Para ativar o profiling é necessário executar o comando setProfilingLevel() no banco de dados devmedia – continue com o uso do Mongo Shell. O método setProfilingLevel() aceita até dois parâmetros. O primeiro é referente ao nível de profiling (0, 1 ou 2). O segundo parâmetro recebe o tempo de resposta a ser observado nas consultas. Para adicionar um nível de profiling ao banco de dados devmedia, use o comando db.setProfilingLevel(1, 60). Deste modo ativaremos o nível 1, e o log irá registrar todas as consultas com tempo de resposta superior a 60 milissegundos. Se desejar confirmar qual o nível de profiling está sendo usado pelo MongoDB, digite o comando db.getProfilingStatus(). Neste caso a resposta seria {"was" : 1, "slowms" : 60}. O campo was se refere ao nível de profiling e o campo slowms, o tempo mínimo em milissegundos para que o log passe a registrar as consultas.

O profiling só registra consultas após ser ativado, então, para verificar as consultas demonstradas até agora no artigo, será preciso executá-las novamente. Feito isso, rode uma consulta sobre o log como o exemplo a seguir: db.system.profile.find().limit(1).sort( { millis : -1 } ).pretty(). Neste comando estamos procurando pela consulta que possui o maior tempo de resposta, para então criar um índice que possa diminuir esse tempo. Vale ressaltar que quando um índice é criado ele não será utilizado apenas para uma única consulta. Veja na Listagem 22 a consulta retornada como sendo a mais demorada entre todas.

Listagem 22. Consulta por maior tempo de resposta segundo o profiling log.

> db.system.profile.find().limit(1).sort( { millis : -1 } ).pretty() { "ts" : ISODate("2013-01-05T18:35:43.830Z"), "op" : "query", "ns" : "devmedia.users", "query" : { "query" : { "$and" : [

(23)

{ "name" : { "$regex" : "Marcio" } }, { "age" : 25 } ] }, "$explain" : true }, "ntoreturn" : 0, "ntoskip" : 0, "nscanned" : 100000, "keyUpdates" : 0, "numYield" : 0, "lockStats" : { "timeLockedMicros" : { "r" : NumberLong(100953), "w" : NumberLong(0) }, "timeAcquiringMicros" : { "r" : NumberLong(2), "w" : NumberLong(4) } }, "nreturned" : 1, "responseLength" : 383, "millis" : 101, "client" : "127.0.0.1", "user" : "" }

Observada a resposta do log, existem algumas informações importantes a destacar, como: ts, que representa a data e o horário de quando a consulta foi armazenada no log; op é o tipo de ação, que nesse caso foi uma query (consulta), mas que poderia ser também outra operação de CRUD; ns é o nome do banco de dados seguido pelo nome da coleção em que foi realizada a consulta; query é a própria consulta armazenada no log; nscanned é o número de documentos que foram percorridos para a consulta localizar o resultado; e millis é o tempo em milissegundos que levou para a consulta encontrar o resultado.

Até este momento, segundo o profiling, esta é a consulta mais demorada realizada na coleção Users. Foi preciso percorrer cem mil documentos para encontrar um único

documento e levou aproximadamente 101 milissegundos para esta operação. Para melhorar o desempenho desta consulta, vamos criar um índice.

Veja que a consulta em questão possui duas chaves como parâmetros, name e age, portanto, precisamos criar um índice sobre as chaves name e age da coleção Users. Para

(24)

isso é necessário digitar o seguinte comando: db.users.ensureIndex({ name : 1}). Deste modo é adicionada à coleção Users um índice referente à chave name. Vamos também adicionar um índice para a chave age, com o comando db.users.ensureIndex({ age : 1}). Note que o comando ensureIndex() possui uma chave e um valor. O valor especificado para as chaves name e age representa a ordem que o Mongo adotará para armazenar os índices. Quando o valor for 1, será em ordem ascendente e quando for -1, descendente. Para verificar quais índices existem na coleção, utilize o comando db.users.getIndexes(). O resultado deste comando pode ser conferido na Listagem 23.

Listagem 23. Exibindo os índices adicionados à coleção Users.

[ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "devmedia.users", "name" : "_id_" }, { "v" : 1, "key" : { "name" : 1 }, "ns" : "devmedia.users", "name" : "name_1" }, { "v" : 1, "key" : { "age" : 1 }, "ns" : "devmedia.users", "name" : "age_1" } ]

Analisando o código da Listagem 23 é possível observar que a coleção Users possui três índices, sendo um deles chamado de _id_, referente à chave _id. O MongoDB sempre criará um índice para a chave _id automaticamente. Também existem os índices name_1, para a chave name e age_1 para a chave age. Agora que os índices estão adicionados à coleção, já é possível verificar o novo desempenho da consulta por nome e idade. Desta vez, um novo comando será apresentado, o explain(). Este é um tipo de comando estatístico de consultas que possui informações semelhantes ao resultado de uma consulta sobre o profiling log. A diferença aqui é que devemos executar este comando juntamente com a consulta que será analisada, como demonstra o código a seguir:

(25)

db.users.find({

"$and" : [

{ "name" : { "$regex" : "Marcio"} } ,

{ "age" : 25 }

]

}).explain()

O resultado do comando, seguido pelo explain(), será uma estatística referente à consulta, semelhante ao resultado exibido na Listagem 24.

Listagem 24. Resultado do comando explain() na consulta por nome e idade.

{

"cursor" : "BtreeCursor age_1", "isMultiKey" : false, "n" : 17, "nscannedObjects" : 1621, "nscanned" : 1621, "nscannedObjectsAllPlans" : 3242, "nscannedAllPlans" : 4864, "scanAndOrder" : false, "indexOnly" : false, "nYields" : 0, "nChunkSkips" : 0, "millis" : 11, "indexBounds" : { "age" : [ [ 25, 25 ] ] }, "server" : "MarcioBallem-PC:27017" }

Ao analisar o resultado de explain() sobre a consulta por nome e idade é possível visualizar algumas informações importantes, como: a chave cursor informa que o Mongo usou o índice age_1 para realizar a consulta. Sempre que o cursor for do tipo BtreeCursor, significa que um índice foi usado na consulta. Se o cursor for do tipo BasicCursor, significa que nenhum índice foi usado. Ainda sobre a análise, o n indica o número de documentos retornados por esta consulta; nscanned significa o número de documentos percorridos para encontrar o resultado; e a chave millis é o tempo que levou para a consulta encontrar o resultado.

(26)

encontrado no comando explain(), melhor será o desempenho da consulta. Isto porque menos documentos serão escaneados pelo Mongo em busca do resultado requerido.

Dito isso, podemos tirar as seguintes conclusões: entre os índices por nome e idade, o Mongo selecionou o índice por idade como mais eficiente; a quantidade de documentos percorridos antes de ter o índice foi de 100.000, ou seja, todos os documentos foram percorridos, e após a inclusão do índice o Mongo percorreu apenas 1.621, o que torna a consulta mais rápida; por fim, o tempo de retorno diminuiu aproximadamente 10 vezes em relação à consulta sem índice, que era de 101 milissegundos, e passou a ser de 11

milissegundos.

O MongoDB também fornece o comando hint(), para forçá-lo a usar um índice específico, e assim, ele não escolhe qual índice usar. Este comando também pode forçar o Mongo a não usar nenhum índice. Por exemplo, na consulta da Listagem 24 o Mongo escolheu o índice por idade (age_1), porém se o desenvolvedor, por algum motivo, quisesse usar o índice por nome (name_1), teria que executar o seguinte comando:

> db.users.find({

"$and" : [

{ "name" : { "$regex" : "Marcio"} } ,

{ "age" : 25 }

]

}).hint( { name:1 } ).explain()

Desta forma o MongoDB não selecionaria um índice pelo seu sistema de critérios de desempenho. Outra possibilidade é executar a consulta sem o uso de nenhum índice. Para isso, se pode usar o operador $natural como parâmetro do método hint(). Veja um exemplo disso no código:

> db.users.find({

"$and" : [

{ "name" : { "$regex" : "Marcio"} } ,

{ "age" : 25 }

]

(27)

Para cancelar o profiling é preciso desativá-lo com o comando db.setProfilingLevel(0, 60). Caso queira excluir o log, use o comando db.system.profile.drop(). Um profiling só pode ser excluído quando ele já estiver desativado. Outro comando útil é o de exclusão de índices. Se for necessário excluir um índice, use o comando db.users.dropIndex(‘nome_do_index’).

Se você estiver familiarizado com SQL, o framework de agregação do MongoDB fornece funcionalidades semelhantes à Group By, Order By, Where e operadores afins do SQL, assim como formas simples de “joins”. O objetivo do framework é capturar informações entre diversos documentos para retornar um resultado de forma agrupada. Usando o conceito de agregação, é possível adicionar à consulta campos temporários, que não existem nos documentos reais, mas que podem armazenar algum tipo de informação durante a

execução da consulta. Como exemplo, podem-se citar os campos de cálculos, como média e soma. Esses campos não existem nos documentos da coleção Users, mas podemos

adicioná-los na consulta para calcular a média de idade entre os usuários, ou mesmo somar a quantidade de usuários que têm preferência por ‘Futebol’.

O resultado encontrado pela média ou pela soma deve ser armazenado em um campo temporário, do qual o resultado final será extraído. Porém, quando se usa a agregação os resultados não serão documentos como nas consultas simples e avançadas apresentadas nas seções anteriores. Os resultados de uma agregação serão valores definidos por chaves da própria consulta, e o retorno, em Java, é um objeto do tipo java.lang.Iterable, o qual contém as chaves adicionadas às consultas de agregação.

A Tabela 2 apresenta uma comparação entre os operadores de agregação do MongoDB e SQL.

(28)

Tabela 2. Correspondência entre operadores de agregação MongoDB e SQL.

Agora que nos foi apresentado os operadores de agregação e a correspondência deles em relação aos comandos SQL, vamos testar algumas consultas na coleção Users. A primeira consulta com agregação que será realizada é bem sim e terá como objetivo somar todas as idades entre todos os documentos, como também retornar a média entre todas estas idades.

Vamos começar analisando a consulta que seria executada no Mongo Shell, apresentada na Listagem 25. O primeiro passo para usar a agregação é utilizar o método aggregate(), o qual recebe um array com operadores de agregação. Na consulta da Listagem 25 é usado o operador $group para agrupar os documentos pelo identificador _id e depois executar a soma sobre as idades e em seguida verificar a média entre elas. Quando a chave _id recebe o valor null, significa que a consulta deve agrupar todos os documentos, como se eles se tornassem um só documento. O operador $group sempre vai exigir a existência de um _id declarado na consulta. Porém este _id não está relacionado à chave _id dos documentos, ele é um identificador da própria consulta. O resultado da consulta pode ser visualizado na Listagem 26. Note que temos um único resultado resumido em um único documento de _id igual a 0. Em sum_age temos a soma de todas as idades e em avg_age a média entre elas.

As Advanced Queries não poderiam realizar esse tipo de consulta, embora elas possam utilizar os operadores $sum e $avg. Se estes operadores fossem usados em uma consulta avançada, seriam aplicados sobre os valores de algum campo do documento, como por exemplo, somar um campo de preço com um campo de juros, mas nunca somar valores de documentos diferentes.

Listagem 25. Consulta de Agregação – Soma e Média de Idade.

> db.users.aggregate([ { "$group" : {

"_id" : null ,

"sum_age" : { "$sum" : "$age"} , "avg_age" : { "$avg" : "$age"} }

} ])

Listagem 26. Resultado – Consulta Soma e Média de Idade.

{

"result" : [ {

(29)

"_id" : 0, "sum_age" : 4549195, "avg_age" : 45.49195 } ], "ok" : 1 }

Para ficar mais claro o uso da chave _id na agregação, uma pequena mudança na consulta será realizada. Ao invés do identificador receber o valor null, vamos agrupar pela chave age. Dessa forma, o resultado encontrado será dividido entre vários documentos, onde cada documento retornado será correspondente a uma idade específica, com o total e a média de idade referente para cada documento. Confira na Listagem 27 a consulta de agregação alterada e na Listagem 28 o novo resultado.

Nesta nova consulta ainda foi adicionado o operador $limit, para limitar o resultado em apenas três documentos, e também foi adicionado um campo count, para contar o número de documentos referentes a cada idade, e assim seja possível confirmar que o resultado de sum_age dividido por count resulta na média encontrada. É importante observar também, que dentro de um $group as chaves de um documento serão sempre precedidas pelo caractere dólar (“$”).

Listagem 27. Consulta de Agregação modificada – Soma e Média por Idade.

db.users.aggregate([ { "$group" : {

"_id" : "$age" ,

"sum_age" : { "$sum" : "$age"} , "avg_age" : { "$avg" : "$age"}, "count" : { "$sum" : 1 } }

},

{$limit : 3} ])

Listagem 28. Novo resultado – Consulta Soma e Média por Idade.

{

"result" : [ {

"_id" : 66, // idade igual a 66 anos.

(30)

"avg_age" : 66, // média entre a soma das idades. "count" : 1580 // a quantidade de usuários com 66 anos. }, { "_id" : 21, "sum_age" : 34860, "avg_age" : 21, "count" : 1660 }, { "_id" : 18, "sum_age" : 29934, "avg_age" : 18, "count" : 1663 } ], "ok" : 1 }

Vamos analisar agora a consulta da Listagem 25, demonstrada em Mongo Shell, mas implementada em código Java. Para isso, observe na Listagem 29 o método

showSumAndAvgByAge() da classe UserDao. Nesta consulta é necessário primeiro criar um objeto DBObject que receba os valores usados como critérios dentro do operador $group. Após isso, um novo objeto DBObject é instanciado para se adicionar a ele o operador $group como chave e os critérios definidos anteriormente como valor. Até este ponto, a consulta é muito semelhante às anteriores, sem o uso de agregação. A diferença fica por conta de uma nova classe, a com.mongodb.AggregationOutput, ponto central do framework de agregação. O objeto db, abordado na seção sobre a classe de conexão, fornece acesso ao método aggregate(), que recebe como parâmetros os objetos que representam os operadores de agregação em Java. No exemplo do método showSumAndAvgByAge(), é atribuído como parâmetro o objeto group.

Listagem 29. Código do método showSumAndAvgByAge() – Classe UserDao.

public Iterable<DBObject> showSumAndAvgByAge() {

DBObject groupFields = new BasicDBObject(); groupFields.put("_id", null);

groupFields.put("sun_age", new BasicDBObject("$sum", "$age")); groupFields.put("avg_age", new BasicDBObject("$avg", "$age"));

DBObject group = new BasicDBObject("$group", groupFields);

AggregationOutput output = db.aggregate(group);

(31)

}

Imagine agora que se deseja localizar a quantidade de usuários que possuem determinada preferência, ou seja, montar uma consulta que retorne o número de usuários que têm como preferência “Música” e/ou “Livros”. Para isso é preciso usar o operador de agregação

$macth, que tem função semelhante à cláusula where do SQL. Outro operador importante nesta consulta será o $unwind, para realizar a pesquisa sobre o array de preferências. O $unwind tem como objetivo quebrar o array em partes e assim o framework de agregação consegue separar os resultados entre cada preferência contida no array.

Na Listagem 30 é possível visualizar a consulta citada em Mongo Shell. Observe que ela começa quebrando o array em partes com o operador $unwind, e logo em seguida o operador $match indica quais preferências estão sendo usadas como critério de pesquisa. O operador $in já foi abordado anteriormente e trabalha nesta consulta com o mesmo

objetivo. O próximo passo é agrupar os resultados, sendo necessário para isso fazer uso do operador $group. Como o objetivo da consulta é agrupar por preferência individualmente, o _id do operador $group terá como valor a chave $preferences. O campo count irá receber a quantidade de cada preferência. Esta quantidade é calculada através do operador $sum. O valor 1 adicionado ao $sum é um operador de incremento, ou seja, para cada preferência encontrada soma-se mais um. Por fim, o $sort faz com que o resultado seja ordenado pelo campo count de forma decrescente. O valor -1, indicado após o operador $sum, sinaliza que os valores serão ordenados, por exemplo, de “9 – 0” ou “z – a”. Se o valor fosse 1, a ordenação seria de forma crescente, ou seja, de “0 – 9” ou “a – z”. Confira na Listagem 31 o resultado obtido após rodar a consulta da Listagem 30.

Algo importante em relação a esse tipo de consulta é que ela funciona de maneira

estruturada. Cada parte seguinte da consulta depende do resultado da parte anterior. Então cada operador de agregação irá trabalhar com o resultado do operador anterior a ele.

Listagem 30. Consulta de Agregação – Pesquisa por preferências.

> db.users.aggregate([

{ "$unwind" : "$preferences"} , { "$match" :

{ "preferences" : { "$in" : [ "Carpintaria" , "Musica" , "Fotografia"]}} } , { "$group" : { "_id" : "$preferences" , "count" : { "$sum" : 1} } } , { "$sort" : { "count" : -1}}

(32)

])

Listagem 31. Resultado – Consulta por preferências.

{ "result" : [ { "_id" : "Fotografia", "count" : 8755 }, { "_id" : "Musica", "count" : 8703 }, { "_id" : "Carpintaria", "count" : 8634 } ], "ok" : 1 }

Vamos analisar agora a Listagem 32, que demonstra o código Java referente à consulta da Listagem 30. Este código deve seguir a estrutura utilizada no Mongo Shell, isto é,

organizar a consulta de forma estruturada. Sendo assim, o primeiro passo é criar um objeto DBObject para representar o operador $unwind. Após isso, montar um array com a lista de preferências selecionadas. Neste caso, usamos um objeto do tipo BasicDBList. Depois um novo objeto DBObject é instanciado para representar o operador $match e receber o array contendo a lista de preferências. É necessário neste momento criar um objeto que

represente os critérios internos do operador $group, e para isso instanciamos o DBObject groupFields, informando o _id como agrupador e o operador $sum para a soma da quantidade de preferências. Feito isso, o objeto groupFields deve ser atribuído como valor do objeto que contém como chave o operador $group. O último operador de agregação a ser usado é o $sort, também em um objeto do tipo DBObject. Para finalizar, basta adicionar os objetos referentes a cada operador de agregação no método db.aggregate(), respeitando a ordem da esquerda para a direita, que será respectivamente do primeiro operador de agregação para o último.

Listagem 32. Código do método showCountByPreferences() – Classe UserDao.

public Iterable<DBObject>

showCountByPreferences(Collection< String> preferences) {

BasicDBObject unwind = new BasicDBObject ("$unwind", "$preferences");

(33)

array.addAll(preferences);

DBObject match = new BasicDBObject(); match.put("$match", new BasicDBObject

("preferences", new BasicDBObject("$in", array)));

DBObject groupFields = new BasicDBObject(); groupFields.put("_id", "$preferences");

groupFields.put("count", new BasicDBObject("$sum", 1));

DBObject group = new BasicDBObject("$group", groupFields);

DBObject sort = new BasicDBObject

("$sort", new BasicDBObject("count", -1));

AggregationOutput output = db.aggregate (unwind, match, group, sort);

return output.results(); }

Para finalizar os exemplos de consultas com agregação, vamos usar como critério de consulta o campo hometown do documento interno info. O objetivo dessa consulta será exibir todas as profissões existentes em uma determinada cidade, de forma que o resultado retornado mostre a quantidade de usuários que cada profissão possui (nesta cidade).

Veja na Listagem 33 que são usados três operadores de agregação: 1) $match, como critério de seleção da cidade a ser localizada; 2) $group, para agrupar o resultado e somar a quantidade de usuários por profissão; e 3) $sort, para ordenar o resultado por

quantidade. O operador $group apresenta desta vez uma nova forma de se trabalhar com a chave _id. Note que o _id é formado por dois campos e não por apenas um, como visto até o momento. Isto se chama chave composta (compound key).

Como os resultados são agrupados por regra, pela chave _id de $group, usando um _id composto, logicamente o resultado será agrupado conforme as chaves selecionadas neste _id. Por exemplo, retornando à Listagem 27, o valor da chave _id de $group é a chave $age dos documentos. Neste caso todos os resultados da consulta foram agrupados por diferentes idades. Assim, tivemos diversos resultados, cada um referente a uma idade. Quando agrupamos por um _id composto, os resultados serão também agrupados respeitando as chaves atribuídas a este _id, algo semelhante ao Group By do SQL.

Entretanto, no SQL agrupamos os resultados por colunas informadas logo após a instrução Group By. Já no MongoDB os resultados são agrupados através da chave _id. Visualize uma parte do resultado desta consulta na Listagem 34. O restante foi omitido em virtude do

(34)

grande número de informações retornado.

Listagem 33. Consulta de Agregação – Pesquisa por hometown.

> db.users.aggregate([ { "$match" :

{ "info.hometown" : "Angra dos Reis" } } , { "$group" : { "_id" : { "hometown" : "$info.hometown" , "job" : "$info.job" } , "count" : { "$sum" : 1} } } , { "$sort" : { "count" : 1} } ])

Listagem 34. Resultado – Pesquisa por hometown.

{

"result" : [ {

"_id" : {

"hometown" : "Angra dos Reis", // cidade a ser pesquisada. "job" : "Interprete"

// uma das profissões encontrada na cidade. },

"count" : 1 // total de usuários que trabalham como Interprete na cidade. },

{

"_id" : {

"hometown" : "Angra dos Reis", "job" : "Programador" }, "count" : 1 }, { "_id" : {

"hometown" : "Angra dos Reis", "job" : "Corretor de Imoveis" }, "count" : 2 }, ... ], "ok" : 1 }

(35)

Na Listagem 35 temos a consulta em Java referente à Listagem 33. Em relação às consultas de agregação em Java demonstradas anteriormente, a novidade no método showCountUsersByHometown() fica apenas por conta da chave composta, representada pelo objeto DBObject id. Veja neste objeto que foram atribuídas as duas chaves, hometown e job, para criar o id composto. O restante já foi explicado nas listagens anteriores e não precisa de um novo detalhamento.

Listagem 35. Código do método showCountUsersByHometown() – Classe UserDao.

public Iterable<DBObject>

showCountUsersByHometown(String city) { DBObject match = new BasicDBObject

("$match", new BasicDBObject("info.hometown", city));

DBObject id = new BasicDBObject(); id.put("hometown", "$info.hometown"); id.put("job", "$info.job");

DBObject groupFields = new BasicDBObject(); groupFields.put("_id", id);

groupFields.put("count", new BasicDBObject("$sum", 1));

DBObject group = new BasicDBObject("$group", groupFields);

DBObject sort = new BasicDBObject("$sort", new BasicDBObject("count", 1));

AggregationOutput output = db.aggregate(match, group, sort);

return output.results(); }

Uma observação sobre o desempenho das consultas de agregação é que nas versões anteriores ao driver-mongo-java 2.10.0, elas eram consideradas lentas, e muitas vezes se deixava de utilizá-las por consequência disto. Entretanto, segundo os projetistas do MongoDB, após o lançamento da versão 2.10.0 do driver, este desempenho foi melhorado significativamente.

Além dos tipos de consulta já apresentados, existe ainda uma maneira muito prática para montar queries, chamada QueryBuilder. A QueryBuilder é uma classe encontrada no pacote com.mongodb construída com base no padrão de projeto Builder. Uma definição muito comum para este padrão é que ele separa o processo de construção da estrutura de um objeto, de forma que esse processo possa criar diferentes representações.

Para usar os métodos da classe QueryBuilder, primeiro deve ser invocado o método estático start(), e a partir dele outros métodos podem ser invocados, tornando a construção de

Imagem

Referências