Consultas com QueryBuilder MongoDB

O driver-mongo-java do banco de dados MongoDB, fornece uma classe muito interessante para montar as consultas a banco. Esta classe é a QueryBuilder. Para quem já trabalhou com Hibernate, conhece a diferença entre uma consulta em HQL e uma consulta usando a API Criteria. A QueryBuilder do MongoDB é semelhante a API Criteria, porque ela possui métodos direcionados a consulta. Por exemplo, se você deseja fazer uma consulta usando o operador $and, basta chamar o método and(), se pretende usar o operador $gt, basta então usar o método greaterThan() e assim por diante. Neste tutorial será demonstrado como usar a classe QueryBuilder e alguns de seus métodos.

1. Preparando o projeto

Para este tutorial vamos usar a versão 2.10.1 do driver MongoDB para Java:

2. Iniciando o projeto

Os exemplos de consultas serão executados na coleção contatos, que será disponibilizada para download junto com o projeto. Seus documentos serão semelhantes ao seguinte documento:

{  
    '_id' : '03',
    'nome' : 'Joao Carlos Rios Ruzinsky',
    'idade' : 28,
    'tarefas' : [ 'Futebol', 'Basquete' ]
}

Veja na Listagem 1 a classe Contato que representará o documento no projeto Java.

Listagem 1. Classe Contato.
package com.wp.mb.tutorial.entity;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * https://www.mballem.com/
 */
public class Contato implements Serializable {

    private String id;
    private String nome;
    private Integer idade;
    private List tarefas;

    //gere os métodos get/set

    @Override
    public String toString() {
        return "Contato{" +
                "id='" + id + ''' +
                ", nome='" + nome + ''' +
                ", idade='" + idade + ''' +
                ", tarefas=" + tarefas +
                '}';
    }
}

Em seguida teremos a classe ContatoConverter, responsável em converter o resultado da consulta em um objeto Java do tipo Contato, veja na Listagem 2.

Listagem 2. Classe ContatoConverter.
package com.wp.mb.tutorial.converter;

import com.mongodb.DBObject;
import com.wp.mb.tutorial.entity.Contato;

import java.util.List;

/**
 * https://www.mballem.com/
 */
public class ContatoConverter {

    public static Contato converterToContato(DBObject dbo) {
        Contato contato = new Contato();

        contato.setId((String) dbo.get("_id"));
        contato.setNome((String) dbo.get("nome"));
        contato.setIdade((Integer) dbo.get("idade"));
        contato.setTarefas((List) dbo.get("tarefas"));

        return contato;
    }
}

Na Listagem 3 temos a interface IContatoDao, que possui apenas um método de consulta. Este método tem como retorno uma lista de contatos.

Listagem 3. Interface IUserDAO.
package com.wp.mb.tutorial.dao;

import com.mongodb.DBObject;

import java.io.Serializable;
import java.util.List;

/**
 * https://www.mballem.com/
 */
public interface IContatoDao {

    List findContatos(DBObject dbo);
}

A implementação da interface IContatoDao fica por conta da classe ContatoDao na Listagem 4. O método findContatos() recebe um objeto do tipo DBObject (nativo do mongo-driver) como argumento, o qual conterá a consulta montada através da QueryBuilder.

Listagem 4. Classe ContatoDao.
package com.wp.mb.tutorial.dao;

import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.wp.mb.tutorial.converter.ContatoConverter;
import com.wp.mb.tutorial.entity.Contato;

import java.util.ArrayList;
import java.util.List;

/**
 * https://www.mballem.com/
 */
public class ContatoDao implements IContatoDao {

    private DBCollection db;

    public ContatoDao() {
        this.db = MongoConnection.getInstance().getDB().getCollection("contatos");
    }

    public List findContatos(DBObject dbo) {

        List contatos = new ArrayList();

        DBCursor cursor = db.find(dbo);

        while (cursor.hasNext()) {
            contatos.add(ContatoConverter.converterToContato(cursor.next()));
        }

        return contatos;
    }
}

Na Listagem 5 é possível conferir a classe de conexão com o MongoDB, chamada MongoConnection. Esta classe foi construída com base no padrão Singleton, o qual tem como objetivo fornecer uma única instancia da classe durante todo o ciclo de vida da aplicação. O acesso a esta instancia fica por conta de um método estático chamado getInstance(). O nome do banco de dados será: db-contato.

Listagem 5. Classe MongoConnection.
package com.wp.mb.tutorial.dao;

import com.mongodb.DB;
import com.mongodb.MongoClient;

import java.net.UnknownHostException;

/**
 * https://www.mballem.com/
 */
public class MongoConnection {
    private static final String HOST = "localhost";
    private static final int PORT = 27017;
    private static final String DB_NAME = "db-contato";

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

    private MongoClient mongo;
    private DB db;

    private MongoConnection() {
        //construtor privado
    }

    //garante sempre uma unica instancia da classe
    public static synchronized MongoConnection getInstance() {
        if (uniqInstance == null) {
            uniqInstance = new MongoConnection();
        }
        return uniqInstance;
    }

    //garante um unico objeto mongo
    public DB getDB() {
        if (mongo == null) {
            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;
    }
}

3. Implementando as consultas com QueryBuilder

A primeira consulta que vamos testar será baseada em uma busca por nome, veja na Listagem 6. Devemos criar um objeto do tipo DBObject que receberá a consulta QueryBuilder. O primeiro método a ser usado é o start() que indica por qual chave será a consulta. O método is() recebe o parametro da consulta, no caso o nome do contato. Este método seria equivalente ao like do SQL. Já método get() transforma a consulta em um objeto DBObject. Em seguida fazemos uma chamada ao método findContatos(), da interface IUserDao, e passamos como par metro a variável query, que contem a consulta.

Listagem 6. Classe ContatoTeste – Método findByNome().
public class ContatoTeste {

    private IContatoDao dao = new ContatoDao();

    public static void main(String[] args) {
        ContatoTeste t = new ContatoTeste();

        t.findByName();

        //t.findByNameAndIdade();

        //t.findByTarefas();
    }

    private void findByName() {
        DBObject query = QueryBuilder
                .start("nome")
                .is("Bruna de Melo Pereira")
                .get();

        List contatos = dao.findContatos(query);

        for (Contato contato : contatos) {
            System.out.println(contato.toString());
        }
    }
}
Contato{id='44', nome='Bruna de Melo Pereira', idade='25', tarefas=[ "Musica"]}

A próxima consulta (Listagem 7) a ser analisada usará os métodos greaterThanEquals() – maior igual que – o método lessThanEquals() – menor igual que – e o método and(). Veja que o método and() é seguido por um método regex(), que possui a função de expressão regular. Esta consulta então irá selecionar todos os contatos entre ‘25’ e ‘30’ de idade, e que contenham como parte do nome o sobrenome ‘Souza’.

Listagem 7. Classe ContatoTeste – Método findByNomeAndIdade().
private void findByNomeAndIdade() {
	DBObject query = QueryBuilder.start("idade")
			.greaterThanEquals(25)
			.lessThanEquals(30)
			.and("nome").regex(Pattern.compile("Souza"))
			.get();

	List contatos = dao.findContatos(query);

	for (Contato contato : contatos) {
		System.out.println(contato.toString());
	}
}
Contato{id='04', nome='Ana Carla de Souza', idade='25', tarefas=[ "Culinaria" , "Filmes"]}

Contato{id='07', nome='Joao Antonio de Souza', idade='25', tarefas=[ "Veterinaria" , "Voo Livre"]}

Contato{id='18', nome='Marus Daniel Souza e Castro', idade='26', tarefas=[ "Cinema" , "Teatro"]}

Contato{id='23', nome='Miguel Antunes de Souza', idade='29', tarefas=[ "Poker" , "Bilhar"]}

Contato{id='32', nome='Milton Onorio de Souza', idade='27', tarefas=[ "Pescaria" , "Surf"]}

Na Listagem 8 vamos trabalhar com o campo tarefas do documento, que é um campo do tipo array. No MongoDB temos três possibilidades de consulta sobre um campo do tipo array, veremos a seguir a diferença entre as três.

Na primeira consulta, referente ao objeto is, foi informado como parametros um array contendo duas tarefas. O retorno dessa consulta será documento(s) que tenham no campo tarefas a seqüência exata de tarefas passadas como parametro. Ou seja, “Teatro e Cinema”. Se existir um documento que contenha “Cinema, Teatro” ou “Teatro” ou “Cinema” ou “Teatro, Novela, Cinema”, estes não serão retornar, porque não existe a seqüência informada como parametro.

A próxima consulta, do objeto in, irá retornar todos os documentos que contenham as tarefas “Teatro” E/OU “Cinema”.

A última consulta, do objeto all, irá retornar apenas os documentos que contenham “Teatro” e “Cinema”, independentemente da ordem que eles se encontrem nos documentos.

Listagem 8. Classe ContatoTeste – Método findByTarefas().
private void findByTarefas() {
	// Busca Apenas quando houver a sequencia Teatro e Cinema
	DBObject is = QueryBuilder.start("tarefas")
			.is( new String[] {"Teatro", "Cinema"} ).get();

	List contatos = dao.findContatos(is);

	for (Contato contato : contatos) {
		System.out.println("is : " + contato.toString());
	}

	// Busca Teatro E/Ou Cinema
	DBObject in = QueryBuilder.start("tarefas")
			.in( new String[] {"Teatro", "Cinema"} ).get();

	contatos = dao.findContatos(in);

	for (Contato contato : contatos) {
		System.out.println("in : " + contato.toString());
	}

	// Busca Teatro E Cinema
	DBObject all = QueryBuilder.start("tarefas")
			.all( new String[] {"Teatro", "Cinema"} ).get();

	contatos = dao.findContatos(all);

	for (Contato contato : contatos) {
		System.out.println("all : " + contato.toString());
	}
}

Saída no console:

is : Contato{id='35', nome='Marta Fill Nunes', idade='23', tarefas=[ "Teatro" , "Cinema"]}

in : Contato{id='08', nome='Mario Nelson Nunes', idade='26', tarefas=[ "Teatro"]}
in : Contato{id='12', nome='Nicole Rios da Silva', idade='22', tarefas=[ "Voleibol" , "Cinema"]}
in : Contato{id='14', nome='Bruna Roque Santos', idade='24', tarefas=[ "Cinema" , "Novelas" , "Teatro"]}
in : Contato{id='18', nome='Marus Daniel Souza e Castro', idade='26', tarefas=[ "Cinema" , "Teatro"]}
in : Contato{id='27', nome='Crisitina Oliveira', idade='33', tarefas=[ "Medicina" , "Cinema"]}
in : Contato{id='30', nome='Antonia Cristina Mel', idade='34', tarefas=[ "Cinema" , "Teatro"]}
in : Contato{id='35', nome='Marta Fill Nunes', idade='23', tarefas=[ "Teatro" , "Cinema"]}
in : Contato{id='40', nome='Lisiane de Almeida', idade='21', tarefas=[ "Artes" , "Teatro"]}

all : Contato{id='14', nome='Bruna Roque Santos', idade='24', tarefas=[ "Cinema" , "Novelas" , "Teatro"]}
all : Contato{id='18', nome='Marus Daniel Souza e Castro', idade='26', tarefas=[ "Cinema" , "Teatro"]}
all : Contato{id='30', nome='Antonia Cristina Mel', idade='34', tarefas=[ "Cinema" , "Teatro"]}
all : Contato{id='35', nome='Marta Fill Nunes', idade='23', tarefas=[ "Teatro" , "Cinema"]}

4. Importando a coleção contatos

A coleção contatos está disponível dentro da pasta file, que se encontra com o projeto. O nome do arquivo a ser importado é contatos.json. Para importar esta coleção inicialize o MongoDB e em seguida, em uma nova janela do console use o seguinte comando:

C:mongo>mongoimport -d db-contato -c contatos < contatos.json

O resultado deverá informar que foram importados 46 documentos, como este:

connected to: 127.0.0.1
Mon Jan 07 18:21:59 imported 46 objects

Saiba mais em:

download project

Ballem

Marcio Ballem é bacharel em Sistemas de Informação pelo Centro Universitário Franciscano em Santa Maria/RS. Tem experiência com desenvolvimento Delphi e Java em projetos para gestão pública e acadêmica. Possui certificação em Java, OCJP 6.

Você pode gostar...