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 (consulte Recursos).
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. E 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. Morphia 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. Consulte Download para obter o código de amostra completo.
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, Distributor e ContactInfo 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.
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.
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.
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. É 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 valornoClassnameStored à 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.
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. O operador = que eu usei na consulta acima, por exemplo, é análogo ao operador $eq no MongoDB. Detalhes completos sobre os operadores de filtro estão disponíveis na documentação online do Morphia (consulte Recursos).
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);
|
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.
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).
| Descrição | Nome | Tamanho | Método de download |
|---|---|---|---|
| Sample code for this article | j-morphia.zip | 17.2KB | HTTP |
Informações sobre métodos de download
Aprender
-
Morphia: Visite o projeto do Morphia no Google Code para saber mais sobre o Morphia.
-
MongoDB: Saiba mais sobre MongoDB.
-
"Desenvolvimento em Java 2.0: MongoDB: Um armazenamento de dados NoSQL com movimentos (apenas os corretos) de RDBMS" (Andrew Glover, developerWorks, setembro de 2010): Um bom artigo introdutório sobre o uso do MongoDB com o driver de linguagem Java.
-
Podcast: Andrew
Glover entrevista Eliot Horowitz, CTO de 10gen, criadores do MongoDB
(developerWorks, setembro de 2010).
-
Navegue na
livraria de tecnologia para ver livros sobre este e outros tópicos técnicos.
-
Zona tecnologia Java do developerWorks: Encontre centenas de artigos sobre cada aspecto da programação Java.
Obter produtos e tecnologias
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.
