NoSQL com MongoDB e Persistência em Java

NoSQL é um tipo de banco de dados, o qual possui uma implementação muito diferente dos bancos de dados relacionais. Foi criado em 1998 por Carlo Strozzi e tem como significado "No only SQL" (Não apenas SQL). Apenas em 2009 seu conceito passou a ser realmente utilizado por necessidades que partiram de grandes instituições como Google, Yahoo, Facebook, Twitter…

Com a grande quantidade de acessos simult neos e o grande consumo físico de armazenamento em disco, estas empresas precisavam de uma solução mais ágil para armazenar e ler os dados de seus clientes. Enquanto bancos relacionais trabalham com relacionamentos entre tabelas e colunas, os bancos NoSQL utilizam coleções de documentos, que armazenam os dados em forma de chave:valor. Este tipo de banco não segue as especificações ACID dos SGDBs tradicionais, o que faz com que eles ganhem em performance e também reduzam o espaço físico em disco.

Outra característica forte é que os dados são geralmente manipulados em memória RAM, o que garante a maior velocidade nas operações de CRUD. Cada banco de dados NoSQL possui uma API própria para o gerenciamento das transações com o banco, e o que veremos neste tutorial será a API Java do MongoDB.

O MongoDB implementa o modelo baseado em documentos, tem foco no tratamento de grandes volumes de dados e é ideal para grande parte das aplicações web. Um dado MongoDB é salvo a partir de um objeto do tipo JSON e internamente transformado em um objeto BSON.

{ "_id" : ObjectId("4b7f13c38dbe0e0f149e0753"), "name" : "Maria", "age" : 40 }

1. API MongoDB

Para realizar o download da API MongoDB acesse: https://github.com/mongodb/mongo-java-driver/downloads e baixe a versão 2.8.0, a qual usaremos neste tutorial. Se você precisa de ajuda para instalar o banco de dados MongoDB, acesse: http://www.mongodb.org/display/DOCS/Quickstart e selecione o seu sistema operacional na seção Installation Guides e siga as instruções.

2. Iniciando o projeto

O projeto exemplo terá duas classes de entidades, Person – Listagem 1 – e Phone – Listagem 2 – onde cada Person, além de alguns dados próprios, poderá cadastrar dois telefones.

Listagem 1. Classe Phone
package com.mballem.simplemongodb.entity;

/**
 * https://www.mballem.com/
 */
public class Phone {
    private String phoneNumber;
    private String mobileNumber;

    public Phone() {
        super();
    }

    public Phone(String phoneNumber, String mobileNumber) {
        this.phoneNumber = phoneNumber;
        this.mobileNumber = mobileNumber;
    }

    //Gere os metodos get/set      

    @Override
    public String toString() {
        return "Phone{" +
                "phoneNumber='" + phoneNumber + '\'' +
                ", mobileNumber='" + mobileNumber + '\'' +
                '}';
    }
}
Listagem 2. Classe Person
package com.mballem.simplemongodb.entity;

import java.io.Serializable;

/**
 * https://www.mballem.com/
 */
public class Person implements Serializable {
    private String id;
    private String firstName;
    private String lastName;
    private int age;
    private Phone phone;

    public Person() {
        super();
    }

    public Person(String firstName, String lastName, int age, Phone phone) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.phone = phone;
    }

    //Gere os metodos get/set

    @Override
    public String toString() {
        return "Person{" +
                "id='" + id + '\'' +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                ", phone=" + phone +
                '}';
    }
}

Vamos usar o padrão Data Access Object (DAO) como modelo de classes de persistência. Para isso, vamos criar primeiro uma classe de acesso ao banco de dados MongoDB. O driver Java MongoDB é thread safe, então, você deve criar uma única inst ncia de Mongo e usá-la em cada solicitação. O objeto Mongo mantém um pool interno de conexões com o banco de dados. Para cada solicitação ao banco de dados (pesquisar, inserir, etc) o thread Java vai obter uma conexão do pool, executar a operação, e liberar a conexão.

Sendo assim, vamos criar uma classe de conexão utilizando o padrão de projeto Singleton, o qual nos proporciona uma forma de ter em todo ciclo de vida da aplicação uma única instancia da classe de conexão – Veja a classe MongoConnection da Listagem 3.

Listagem 3. Classe MongoConnection
package com.mballem.simplemongodb.dao;

import com.mongodb.DB;
import com.mongodb.Mongo;

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 = "simple-mongodb";

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

    private Mongo mongo;
    private DB db;

    private MongoConnection() {
        //construtor privado
    }

    //garante sempre uma unica instancia
    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 Mongo(HOST, PORT);
                db = mongo.getDB(DB_NAME);
                System.out.println("Mongo instance equals :> " + mongoInstance++);
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
        }
        return db;
    }
}

Uma classe do tipo Singleton deve ter um construtor privado, para não permitir que esta classe seja herdada ou instanciada por outra classe qualquer. Seu acesso deve ser através de um método estático, o qual vai liberar sempre uma única instancia de tal classe. Veja no exemplo da Listagem 3, que temos uma variável estática do tipo MongoConnection e o método estático getInstancia() testa se a variável é nula, se ela for, uma instancia é criada, caso contrario se utiliza a mesma instancia da classe sempre que o método for invocado. No primeiro acesso a este método, a variável terá valor nulo, e assim, receberá uma instancia da classe (new MongoConnection). Os demais acessos não receberão uma nova instancia, já que a variável não é mais nula, garantindo assim um única instancia desta classe.

A conexão ao MongoDB é feita por três passos:

  1. Criamos um objeto Mongo atribuindo a ele o endereço do host e a porta de acesso;
  2. A partir do objeto Mongo, acessamos o método getDB(), atribuindo como parametro o nome da base de dados. Caso o banco não exista, ele será criado em tempo de execução;
  3. Por fim, o objeto DB, inicializado pelo método getDB(), possui o método getCollection() para acessar as coleções do banco de dados. Este método deve receber como par metro o nome da coleção, caso esta coleção não exista, será criada na primeira tentativa de acesso a ela;

É a partir de um objeto DBCollecion que temos acesso a todos os metodos de acesso ao banco MongoDB. Podemos ter uma ou varias coleções no banco de dados, dependendo apenas da necessidade de cada projeto. Neste tutorial, vamos criar uma única coleção, chamada Person. Ela irá armazenar documentos contendo um objeto person e phone. Poderíamos também criar uma coleção para Phone, onde poderia ser armazenados os dados apenas de telefones, o que ficaria bem parecido com um modelo relacional um-para-um, mas neste caso não será necessário.

Seguindo o padrão DAO, veja na Listagem 4 a interface IDao. Note que os parametros dos metodos são todos do tipo java.util.Map. Fizemos assim em conseqüência de trabalharmos com chave:valor, e o objeto Map se torna excelente para essa pratica. Já os metodos de consulta possuem como retorno o objeto com.mongodb.DBObject, proprio da API MongoDB o qual deverá ser convertido para a entidade de pesquisa.

Listagem 4. Interface IDao
package com.mballem.simplemongodb.dao;

import com.mongodb.DBObject;

import java.util.List;
import java.util.Map;

/**
 * https://www.mballem.com/
 */
public interface IDao {
    void save(Map mapEntity);

    void update(Map mapQuery, Map mapEntity);

    void delete(Map mapEntity);

    DBObject findOne(Map mapEntity);

    List findAll();

    List findKeyValue(Map keyValue);
}

Vamos agora implementar a interface IDao na classe genérica EntityDao da Listagem 5. Veja que no construtor da classe recebemos como par metro um objeto persistentClass. A partir desse objeto acessamos o método getSimpleName() para recuperar o nome da classe persistente, e assim, criar uma coleção com este nome. Veja também que todos os metodos de CRUD trabalham com objetos do tipo com.mongodb.DBObject ou com.mongodb.BasicDBObject. É através deles que a API transforma os parametros Map em um documento JSON. Após isto os metodos de CRUD da API são acessives através de um objeto DBCollection. Estes metodos recebem como parametros os objetos DBObject ou BasicDBObject.

Listagem 5. Classe genérica EntityDao.
package com.mballem.simplemongodb.dao;

import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;

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

/**
 * https://www.mballem.com/
 */
public class EntityDao implements IDao {

    private Class persistentClass;
    private DBCollection dbCollection;

    public EntityDao(Class persistentClass) {
        this.persistentClass = persistentClass;
        this.dbCollection =
                MongoConnection.getInstance()
                    .getDB().getCollection(persistentClass.getSimpleName());
    }

    protected DBCollection getDbCollection() {
        return dbCollection;
    }

    public void save(Map mapEntity) {
        BasicDBObject document = new BasicDBObject(mapEntity);

        dbCollection.save(document);

        System.out.println("Save :> " + document);
    }

    public void update(Map mapQuery,
                       Map mapEntity) {
        BasicDBObject query = new BasicDBObject(mapQuery);

        BasicDBObject entity = new BasicDBObject(mapEntity);

        dbCollection.update(query, entity);
    }

    public void delete(Map mapEntity) {
        BasicDBObject entity = new BasicDBObject(mapEntity);

        dbCollection.remove(entity);
    }

    public DBObject findOne(Map mapEntity) {
        BasicDBObject query = new BasicDBObject(mapEntity);

        return dbCollection.findOne(query);
    }

    public List findAll() {
        List list = new ArrayList();

        DBCursor cursor = dbCollection.find();

        while (cursor.hasNext()) {
            list.add(cursor.next());
        }

        return list;
    }

    public List findKeyValue(Map keyValue) {
        List list = new ArrayList();

        DBCursor cursor = dbCollection.find(new BasicDBObject(keyValue));

        while (cursor.hasNext()) {
            list.add(cursor.next());
        }

        return list;
    }
}

Agora vamos criar a classe PersonDao (Listagem 6), estendendo a classe EntityDao. No construtor da classe fazemos uma chamada a super(), passando como parametro Person.class, o qual será o nome da coleção a ser criada e posteriormente acessada.

Listagem 6. Classe PersonDao.
package com.mballem.simplemongodb.dao;

import com.mongodb.DBObject;
import com.mballem.simplemongodb.converter.PersonConverter;
package com.mballem.simplemongodb.dao;

import com.mongodb.DBObject;
import com.mballem.simplemongodb.converter.PersonConverter;
import com.mballem.simplemongodb.entity.Person;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * https://www.mballem.com/
 */
public class PersonDao extends EntityDao {

    public PersonDao() {
        super(Person.class);
    }

    public void save(Person person) {
        Map mapPerson =
                new PersonConverter().converterToMap(person);

        save(mapPerson);
    }

    public void update(Person oldPerson, Person newPerson) {
        Map query =
                new PersonConverter().converterToMap(oldPerson);

        Map map =
                new PersonConverter().converterToMap(newPerson);

        update(query, map);
    }

    public void delete(Person person) {
        Map map =
                new PersonConverter().converterToMap(person);

        delete(map);
    }

    public Person findPerson(Map mapKeyValue) {
        DBObject dbObject = findOne(mapKeyValue);

        Person person =
                new PersonConverter().converterToPerson(dbObject);

        return person;
    }

    public List findPersons() {
        List dbObject = findAll();

        List persons = new ArrayList();

        for (DBObject dbo : dbObject) {
            Person person = new PersonConverter().converterToPerson(dbo);

            persons.add(person);
        }

        return persons;
    }

    public List findPersons(Map mapKeyValue) {
        List dbObject = findKeyValue(mapKeyValue);

        List persons = new ArrayList();

        for (DBObject dbo : dbObject) {
            Person person = new PersonConverter().converterToPerson(dbo);

            persons.add(person);
        }

        return persons;
    }
}

Quando invocamos os metodos de EntityDao, precisamos passar como parametro objetos Map, para isso, criamos classes com metodos de conversão, como você pode ver nas Listagens 7 e 8. Essa conversão é necessária para transformar um objeto Person e/ou Phone em um objeto Map. Também é necessário converter o retorno de uma pesquisa, para transformar o retorno que é do tipo DBObject em um objeto Person e/ou Phone.

Listagem 7. Classe PersonConverter.
package com.mballem.simplemongodb.converter;

import com.mongodb.DBObject;
import com.mballem.simplemongodb.entity.Person;

import java.util.HashMap;
import java.util.Map;

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

    public Map converterToMap(Person person) {
        Map mapPerson = new HashMap();
        mapPerson.put("firstName", person.getFirstName());
        mapPerson.put("lastName", person.getLastName());
        mapPerson.put("age", person.getAge());
        mapPerson.put("phone",
                new PhoneConverter().converterToMap(person.getPhone())
        );

        return mapPerson;
    }

    public Person converterToPerson(DBObject dbo) {
        Person person = new Person();
        person.setId(dbo.get("_id").toString());
        person.setFirstName((String) dbo.get("firstName"));
        person.setLastName((String) dbo.get("lastName"));
        person.setAge((Integer) dbo.get("age"));
        person.setPhone(new PhoneConverter().converterToPhone(
                (HashMap) dbo.get("phone"))
        );

        return person;
    }
}
Listagem 8. Classe PhoneConverter.
package com.mballem.simplemongodb.converter;

import com.mongodb.DBObject;
import com.mballem.simplemongodb.entity.Phone;

import java.util.HashMap;
import java.util.Map;

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

    public Map converterToMap(Phone phone) {
        Map mapPhone = new HashMap();
        mapPhone.put("phoneNumber", phone.getPhoneNumber());
        mapPhone.put("mobileNumber", phone.getMobileNumber());

        return mapPhone;
    }

    public Phone converterToPhone(HashMap hashMap) {
        Phone phone = new Phone();
        phone.setPhoneNumber((String) hashMap.get("phoneNumber"));
        phone.setMobileNumber((String) hashMap.get("mobileNumber"));

        return phone;
    }
}

Para executar alguns testes, vamos trabalhar com a classe PersonTest, conforme a Listagem 9. Neste teste, usamos todos os metodos implementados na classe EntityDao, acessando-os através de PersonDao. Antes de executar, não esqueça iniciar o serviço do MongoDB.

Listagem 9. Classe PersonTest.
package com.mballem.simplemongodb;

import com.mballem.simplemongodb.dao.PersonDao;
import com.mballem.simplemongodb.entity.Person;
import com.mballem.simplemongodb.entity.Phone;

import java.util.*;

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

    public static void main(String[] args) {
        save();
        //update();
        //delete();
    }

    private static void save() {
        Phone ph1 = new Phone("021-3222.6598", "021-9145.9966");
        Person p1 = new Person("João Luiz", "de Alvarez", 27, ph1);
        new PersonDao().save(p1);

        Phone ph2 = new Phone("011-3002.0590", "011-9100.9006");
        Person p2 = new Person("João Luiz", "de Souza", 23, ph2);
        new PersonDao().save(p2);

        Phone ph3 = new Phone("055-3222.2522", "055-9225.4464");
        Person p3 = new Person("Anita", "Pires de Almeida", 38, ph3);
        new PersonDao().save(p3);

        List persons = new PersonDao().findPersons();
        for (Person person : persons) {
            System.out.println(person.toString());
        }
    }

    private static void update() {
        Map map = new HashMap();
        map.put("firstName", "Anita");
        Person query = new PersonDao().findPerson(map);

        Phone phone = new Phone("048-3222.2522", "048-9225.4464");
        Person person = new Person("Anita", "Pires de Almeida", 30, phone);
        new PersonDao().update(query, person);

        Person newPerson = new PersonDao().findPerson(map);
        System.out.printf("Old:> " + query + "\nNew:> " + newPerson.toString());
    }

    private static void delete() {
        Map map = new HashMap();
        map.put("firstName", new ObjectId("João Luiz"));
        List query = new PersonDao().findPersons(map);

        for (Person person : query) {
            new PersonDao().delete(person);
        }

        List persons = new PersonDao().findPersons();
        for (Person person : persons) {
            System.out.println(person.toString());
        }
    } 
}

Primeiro execute o método save(), e terá o seguinte resultado no console de sua IDE:

Mongo instance equals :> 1
Save :> { "lastName" : "de Alvarez" , "phone" : { "phoneNumber" : "021-3222.6598" , "mobileNumber" : "021-9145.9966"} , "age" : 27 , "firstName" : "João Luiz" , "_id" : { "$oid" : "4ffb5f166b70dcfdf43971c0"}}

Save :> { "lastName" : "de Souza" , "phone" : { "phoneNumber" : "011-3002.0590" , "mobileNumber" : "011-9100.9006"} , "age" : 23 , "firstName" : "João Luiz" , "_id" : { "$oid" : "4ffb5f166b70dcfdf43971c1"}}

Save :> { "lastName" : "Pires de Almeida" , "phone" : { "phoneNumber" : "055-3222.2522" , "mobileNumber" : "055-9225.4464"} , "age" : 38 , "firstName" : "Anita" , "_id" : { "$oid" : "4ffb5f166b70dcfdf43971c2"}}

Person{id='4ffb5f166b70dcfdf43971c0', firstName='João Luiz', lastName='de Alvarez', age=27, phone=Phone{phoneNumber='021-3222.6598', mobileNumber='021-9145.9966'}}

Person{id='4ffb5f166b70dcfdf43971c1', firstName='João Luiz', lastName='de Souza', age=23, phone=Phone{phoneNumber='011-3002.0590', mobileNumber='011-9100.9006'}}

Person{id='4ffb5f166b70dcfdf43971c2', firstName='Anita', lastName='Pires de Almeida', age=38, phone=Phone{phoneNumber='055-3222.2522', mobileNumber='055-9225.4464'}}

A primeira saída indica que em todo o processo, ou seja, 3 chamadas ao método save() e 1 ao método findAll(), tivemos apenas uma instancia da classe de conexão. Nas saídas save :> temos o objeto em forma de JSON, já com o atributo id gerado automaticamente, uma espécie de auto incremento. Em seguida temos a chamada o método toString() do objeto Person resultante da consulta de findAll(). Agora, comente a chamada ao método save() no método main() e execute apenas o método update(). Após o update(), teste apenas o método delete().

Você também pode fazer um consulta através do ID de um documento, mas para isso você usará a chave como: _id e o valor do ID deve ser informado como um objeto do tipo: org.bson.types.ObjectId(), veja:

Map map = new HashMap();
map.put("_id", new ObjectId("4ffb5f166b70dcfdf43971c1"));

Leia mais em:

Download via Github

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...