Back-End

30 mar, 2012

Persistência de modelo de domínio com Morphia e MongoDB

Publicidade

MongoDB é um banco de dados orientado a documentos para armazenar e
recuperar documentos no estilo JavaScript Object Notation (JSON).
Ampliado com recursos de indexação, replicação e sharding, MongoDB
estabeleceu-se como um competidor NoSQL robusto e escalável.

Um driver Java oficial está disponível para interagir com MongoDB. O driver fornece uma implementação de Map, BasicDBObject, para representar documentos no armazenamento de dados.

Embora a representação Map
seja conveniente, especialmente ao serializar a partir de JSON, poder
representar os documentos como uma hierarquia de classes Java também tem
suas vantagens.

Mapear documentos a partir de um modelo de domínio
Java, por exemplo, permite forçar segurança de tipo na camada Java e ao
mesmo tempo aproveitar os benefícios do desenvolvimento sem esquemas com
MongoDB. Muitas estruturas Java pressupõem o uso de POJOs (plain old
Java objects) ou tem maior capacidade de lidar com eles.

Morphia é um projeto Google Code com licença Apache que permite
tornar persistente, recuperar, excluir e consultar POJOs armazenados
como documentos no MongoDB.

Ele ainda consegue isso fornecendo um conjunto
de anotações e um wrapper em torno do driver Java do Mongo. Morphia tem
conceito semelhante ao de mapeadores relacionais de objetos, tais como
implementações Java Persistence API (JPA) ou Java Data Objects (JDO).

Neste artigo, mostrarei como usar Morphia com um modelo de domínio Java
mapeado ao MongoDB. Ao final do artigo, você pode consultar a tabela de Download para obter o código de amostra completo.

Definindo o modelo de domínio

Usarei um modelo de domínio simplificado para demonstrar a
funcionalidade do Morphia. BandManager, um aplicativo da Web hipotético,
fornece dados sobre bandas musicais: seus membros, distribuidor,
catálogo, gênero e assim por diante.

Definirei as classes Band, Song, ContactInfo e Distributor para representar este modelo de domínio, como mostra a Figura 1:

Figura 1. Classes do BandManager

O diagrama de Unified Modeling Language (UML), na Figura 1, mostra a hierarquia de classes do modelo de domínio. O retângulo à esquerda representa a classe Band.

Empilhados à direita estão retângulos representando as classes ContactInfo, Distributor e Song, respectivamente. Uma seta indo de Band para ContactInfo está marcada com 1 no lado de ContactInfo, indicando uma relação de um a um entre as duas classes.

Uma linha conectando Band a Distributor está marcada no lado de Band com 0..* e com 1 no lado de Distributor, indicando que Band tem apenas um Distributor e que um Distributor representa várias Bands.

Por fim, uma seta indo de Band para Song está marcada no lado de Song com catalog 0..1, indicando que Band tem uma relação de um para muitos com Song e que esta relação é chamada de catalog.

Anotarei essas classes e usarei a interface Datastore do Morphia para salvá-las como documentos no MongoDB.

Anotando o modelo de domínio

A Listagem 1 mostra como a classe Band é anotada:

Listagem 1. Band.java

@Entity("bands")
public class Band {

@Id
ObjectId id;

String name;

String genre;

@Reference
Distributor distributor;

@Reference("catalog")
List<Song> songs = new ArrayList<Song>();

@Embedded
List<String> members = new ArrayList<String>();

@Embedded("info")
ContactInfo info;

A anotação @Entity é necessária. Ela declara que a classe deve ser persistente como um documento em uma coleção dedicada. O valor fornecido à anotação @Entity, bands, define o nome da coleção.

Morphia, por padrão, usa o nome da classe para nomear a coleção. Se eu não incluísse o valor de bands, por exemplo, a coleção seria chamada Band no banco de dados.

A anotação @Id instrui o Morphia sobre qual campo usar como ID do documento. Se você tentar tornar persistente um objeto cujo campo anotado @Id está vazio, o Morphia gera automaticamente um valor de ID.

Morphia tenta tornar persistente qualquer campo sem anotação que encontrar, a menos que estejam marcados com a anotação @Transient. As propriedades name e genre, por exemplo, serão salvas como cadeias de caractere no documento, com chaves de name e genre.

As propriedades distributor, songs, members e info fazem referência a outros objetos. Um objeto membro, a menos que seja anotado com @Reference, como você verá adiante, é considerado integrado.

Aparecerá como um filho no documento pai na coleção. Por exemplo, o members List ficaria assim ao persistir:

"members" : [ "Jim", "Joe", "Frank", "Tom"]

A propriedade info é outro objeto integrado. Neste caso, estou configurando a anotação @Embedded explicitamente com um valor de info. Isso substitui o nome padrão do filho no documento, que de outra forma seria chamado contactInfo. Por exemplo:

"info" : { "city" : "Brooklyn", "phoneNumber" : "718-555-5555" }

Usar a anotação @Reference indica que o objeto é uma referência a um documento em outra coleção. Quando o objeto é carregado da coleção do Mongo, o Morphia segue essas referências para desenvolver o gráfico de objeto. Por exemplo, a propriedade distributor tem este aspecto no documento persistido:

"distributor" : { "$ref" : "distributors", "$id" : ObjectId("4cf7ba6fd8d6daa68a510e8b") }

Assim como a anotação @Embedded, @Reference pode receber um valor para substituir o nome padrão. Neste caso, estou chamando a Lista de songs um catalog no documento.

Tipos de dados: MongoDB oferece suporte a um conjunto menor de tipos de dados que a linguagem Java, a saber integer, long, double e string. Morphia converte tipos de dados Java básicos (tais como float) automaticamente.

@Reference e DBRefs: Por trás das cortinas, o Morphia usa um DBRef do Mongo para referenciar objetos em uma coleção diferente.

Agora observe as definições de classe de Song, Distributor e ContactInfo. A Listagem 2 mostra a definição de Song:

Listagem 2. Song.java

@Entity("songs")
public class Song {

@Id
ObjectId id;

String name;

A Listagem 3 mostra a definição de Distributor:

Listagem 3. Distributor.java

@Entity("distributors")
public class Distributor {

@Id
ObjectId id;

String name;

@Reference
List<Band> bands = new ArrayList<Band>();

A Listagem 4 mostra a definição de ContactInfo:

Listagem 4. ContactInfo.java

public class ContactInfo {


public ContactInfo() {
}

String city;

String phoneNumber;

A classe ContactInfo não tem uma anotação @Entity. Isso é proposital, pois não preciso de uma coleção dedicada para ContactInfo. Instâncias sempre serão integradas em documento band.

Agora que eu defini e anotei o modelo de domínio, vou mostrar como usar o Datastore do Morphia para salvar, carregar e excluir entidades.

Usando o Datastore

A interface Datastore, um wrapper em torno do driver Java do Mongo, é usada para gerenciar entidades no MongoDB. Como Datastore requer uma instância do Mongo para instanciação, qualquer instância do Mongo existente pode ser reutilizada, ou é possível configurar uma de maneira apropriada para o seu ambiente. Aqui está um exemplo da instanciação de um Datastore que se conecta a uma instância do MongoDB local:

Mongo mongo = new Mongo("localhost");
Datastore datastore = new Morphia().createDatastore(mongo, "bandmanager");

Em seguida, criarei uma instância de Band:

Band band = new Band();
band.setName("Love Burger");
band.getMembers().add("Jim");
band.getMembers().add("Joe");
band.getMembers().add("Frank");
band.getMembers().add("Tom");
band.setGenre("Rock");

Agora que tenho uma instância de Band, posso usar datastore para torná-la persistente:

datastore.save(band);

A band deve agora ser salva em uma coleção chamada bands no banco de dados bandmanager. Usando o cliente da interface de linha de comando do Mongo, eu posso examinar para ter certeza (as linhas são partidas neste e em outros exemplos, devido à largura de página deste artigo):

> db.bands.find();
{ "_id" : ObjectId("4cf7cbf9e4b3ae2526d72587"), "className" :
"com.bandmanager.model.Band", "name" : "Love Burger", "genre" : "Rock",
"members" : [ "Jim", "Joe", "Frank", "Tom" ] }

Ótimo! Está lá. Tudo está como deveria para o campo className. Morphia cria automaticamente este campo para registrar o tipo de objeto no MongoDB. Ele é usado primariamente para determinar os tipos de objetos que não são necessariamente conhecidos no tempo de compilação (ao carregar objetos de uma coleção com tipos mistos, por exemplo).

Se isso lhe incomoda, e você tem certeza de que não precisará desta funcionalidade, é possível fazer com que className não seja persistida adicionando o valor no ClassnameStored à anotação @Entity:

@Entity(value="bands",noClassnameStored=true)

Agora eu carregarei Band e indicarei que é igual ao band que eu tornei persistente:

assert(band.equals(datastore.get(Band.class, band.getId())));

O método get() de Datastore permite carregar uma entidade usando seu ID. Não é preciso especificar a coleção ou definir uma cadeia de caractere de consulta para carregar o objeto. Basta dizer a Datastore qual classe você deseja carregar e qual é o seu ID. Morphia faz o resto.

Agora é hora de examinar os objetos em colaboração em um Band. Começarei definindo alguns Songs, e em seguida irei adicioná-los à instância Band que acabo de criar:

Song song1 = new Song("Stairway");
Song song2 = new Song("Free Bird");

datastore.save(song1);
datastore.save(song2);

Ao verificar a coleção de songs no Mongo, devo ver algo assim:

> db.songs.find();
{ "_id" : ObjectId("4cf7d249c25eae25028ae5be"), "className" :
"com.bandmanager.model.Song", "name" : "Stairway" }
{ "_id" : ObjectId("4cf7d249c25eae25038ae5be"), "className" :
"com. bandmanager.model.Song", "name" : "Free Bird" }

Observe que Songs não são referenciadas em band ainda. Irei adicioná-las a band e verei o que acontece:

band.getSongs().add(song1);
band.getSongs().add(song2);

datastore.save(band);

Agora, quando consulto a coleção bands, devo ver algo assim:

{ "_id" : ObjectId("4cf7d249c25eae25018ae5be"), "name" : "Love Burger", "genre" : "Rock", 
"catalog" : [
{
"$ref" : "songs",
"$id" : ObjectId("4cf7d249c25eae25028ae5be")
},
{
"$ref" : "songs",
"$id" : ObjectId("4cf7d249c25eae25038ae5be")
}
], "members" : [ "Jim", "Joe", "Frank", "Tom"] }

Observe como a coleção songs é salva em um array chamado catalog como duas DBRefs.

Uma limitação atual é que objetos referenciados precisam ser salvos antes que outros objetos possam referenciá-los. É por isso que salvei song1 e song2 antes de adicionar a band.

Agora irei excluir song2:

datastore.delete(song2);

Ao consultar a coleção songs, deve ser indicado que song2 está ausente. Mas se você examinar band, verá que a música ainda está lá. O pior é que tentar carregar a entidade band causa uma exceção:

Caused by: com.google.code.morphia.mapping.MappingException: The 
reference({ "$ref" : "songs", "$id" : "4cf7d249c25eae25038ae5be" }) could not be
fetched for com.bandmanager.model.Band.songs

Por enquanto, para evitar este erro, é necessário remover referências a uma música antes de excluí-la manualmente.

Injeção de dependência (DI): Datastore e Mongo são ambos compatíveis com DI. Você não deve ter problemas ao conectá-los a Spring ou Guice, por exemplo. Se possível, você deve configurar cada um como um singleton e compartilhá-los entre beans em colaboração.

Transações: É importante lembrar que o MongoDB não oferece suporte a transações da mesma maneira que a maioria dos sistemas de gerenciamento de banco de dados relacionais. Se o aplicativo precisa coordenar vários encadeamentos gravando ou lendo em uma coleção, é preciso confiar nos recursos de serialização e simultaneidade da linguagem Java.

Consultando

Carregar entidades por seus IDs não irá me levar muito longe. No fim, eu quero poder consultar o Mongo para obter as entidades que desejo.

Em vez de carregar um band por seu ID, irei consultar pelo seu nome. Faço isso criando um objeto Query e especificando um filtro para obter os resultados que eu desejo:

Query query = datastore.createQuery(Band.class).filter("name = ","Love Burger");

Eu especifico a classe que desejo consultar, Band, e um filtro para o método createQuery(). Após definir a consulta, eu posso usar o método asList() para acessar os resultados:

Band band = (Band) query.asList().get(0);

Os operadores de filtro do Morphia são mapeados intimamente aos operadores de consulta usados em consultas do MongoDB. Por exemplo, o operador = que eu usei na consulta acima é análogo ao operador $eq no MongoDB. Detalhes completos sobre os operadores de filtro estão disponíveis na documentação online do Morphia.

Como alternativa para filtrar consultas, o Morphia oferece uma interface fluente para desenvolver consultas. A consulta de interface fluente a seguir, por exemplo, é idêntica à consulta de filtro anterior:

Query query = datastore.createQuery(Band.class).field("name").equal("Love Burger");

É possível usar notação de ponto para consultar objetos integrados.
Aqui está uma consulta que usa notação de ponto e a interface fluente
para selecionar todas as bandas sediadas no Brooklyn:

Query query = datastore.createQuery(Band.class).field("info.city").equal("Brooklyn");

É possível definir mais ainda o conjunto de resultados da consulta. Eu
irei modificar a consulta anterior para classificar as bandas por nome e
limitar os resultados a 100:

Query query = 
datastore.createQuery(Band.class).field("info.city").equal
("Brooklyn").order("name").limit(100);

Indexação

Você notará que, conforme sua coleção crescer, o desempenho das consultas irá diminuir. Coleções do Mongo, assim como tabelas de bancos de dados relacionais, precisam ser indexadas adequadamente para garantir desempenho de consulta razoável.

Anotar uma propriedade com a anotação @Indexed aplica um índice a um campo. Aqui eu crio um índice de ascensão chamado genreName na propriedade genre de um Band:

@Indexed(value = IndexDirection.ASC, name = "genreName")
String genre;

Para aplicar os índices, o Morphia precisa saber quais classes serão
mapeadas. É preciso instanciar o Morphia de maneira um pouco diferente
para assegurar que os índices sejam aplicados. Isso pode ser feito da
seguinte maneira:

Morphia morphia = new Morphia();
morphia.mapPackage("com.bandmanager.model");
datastore = morphia.createDatastore(mongo, "bandmanager");
datastore.ensureIndexes();

A chamada ensureIndexes() final instrui o datastore a criar os índices exigidos se não já existirem.

Índices também podem ser usados para evitar que duplicatas sejam inseridas em uma coleção. Ao configurar a propriedade unique na anotação @Indexed para o nome de um band, por exemplo, eu posso assegurar que apenas um band com o nome dado estará na coleção:

@Indexed(value = IndexDirection.ASC, name = "bandName", unique = true)
String name;

bands posteriores com o mesmo nome seriam excluídos.

Conclusão

Morphia é uma ferramenta poderosa para interagir com o MongoDB. Permite
acesso idiomático com segurança de tipo a documentos do MongoDB. Este
artigo cobriu os aspectos primários ao trabalhar com o Morphia, mas
excluiu alguns recursos.

Eu sugiro que você confira o projeto do Google
Code do Morphia para obter informações sobre seus recursos de suporte,
validação e mapeamento manual de data access object (DAO).

Download

Descrição Nome Tamanho Método de download
Sample code for this article j-morphia.zip 17.2KB HTTP

Recursos

Aprender

Obter produtos e tecnologias

  • Morphia: Faça o download do Morphia.
  • MongoDB: Faça o download do MongoDB.

Discutir

  • Morphia Google Group: Obtenha ajuda de outros usuários do Morphia.
  • Participe da comunidade do developerWorks.
    Entre em contato com outros usuários do developerWorks e explore os
    blogs, fóruns, grupos e wikis voltados para desenvolvedores.

***
artigo publicado originalmente no developerWorks Brasil, por John D’Emic

John D’Emic é Engenheiro de Software Senior em IBM Global Services e
usou MongoDB em diversos contextos de desenvolvimento no ano passado.
Ele é coautor ( com David Dossot) de Mule in Action (Manning Publications, 2009).