1. Información general
En este tutorial, entenderemos cómo usar Morphia, un Object Document Mapper (ODM) para MongoDB en Java.
En el proceso, también entenderemos qué es un ODM y cómo facilita el trabajo con MongoDB.
2. ¿Qué es un ODM ?
Para aquellos no iniciados en esta área, MongoDB es una base de datos orientada a documentos construida para ser distribuida por la naturaleza . Las bases de datos orientadas a documentos, en términos simples, administran documentos, que no son más que una forma sin esquemas de organizar datos semiestructurados . Caen bajo un paraguas más amplio y vagamente definido de bases de datos NoSQL, llamado así por su aparente salida de la organización tradicional de bases de datos SQL.
MongoDB proporciona controladores para casi todos los lenguajes de programación populares como Java . Estos controladores ofrecen una capa de abstracción para trabajar con MongoDB, por lo que no estamos trabajando directamente con Wire Protocol. Piense en esto como Oracle proporcionando una implementación del controlador JDBC para su base de datos relacional.
Sin embargo, si recordamos nuestros días trabajando con JDBC directamente, podemos apreciar lo complicado que puede llegar a ser, especialmente en un paradigma orientado a objetos. Afortunadamente, tenemos marcos de Object Relational Mapping (ORM) como Hibernate para nuestro rescate. No es muy diferente para MongoDB.
Si bien ciertamente podemos trabajar con el controlador de bajo nivel, se requiere mucho más texto estándar para realizar la tarea. Aquí, tenemos un concepto similar al ORM llamado Object Document Mapper (ODM) . Morphia llena exactamente ese espacio para el lenguaje de programación Java y funciona sobre el controlador Java para MongoDB.
3. Configuración de dependencias
Hemos visto suficiente teoría para introducirnos en algún código. Para nuestros ejemplos, modelaremos una biblioteca de libros y veremos cómo podemos administrarla en MongoDB usando Morphia.
Pero antes de comenzar, necesitaremos configurar algunas de las dependencias.
3.1. MongoDB
Necesitamos tener una instancia en ejecución de MongoDB para trabajar. Hay varias formas de conseguirlo, y la más sencilla es descargar e instalar la edición comunitaria en nuestra máquina local.
Debemos dejar todas las configuraciones predeterminadas como están, incluido el puerto en el que se ejecuta MongoDB.
3.2. Morfina
Podemos descargar los archivos JAR prediseñados para Morphia desde Maven Central y usarlos en nuestro proyecto Java.
Sin embargo, la forma más sencilla es utilizar una herramienta de gestión de dependencias como Maven:
dev.morphia.morphia core 1.5.3
4. ¿Cómo conectarse usando Morphia?
Ahora que tenemos MongoDB instalado y en ejecución y hemos configurado Morphia en nuestro proyecto Java, estamos listos para conectarnos a MongoDB usando Morphia.
Veamos cómo podemos lograr eso:
Morphia morphia = new Morphia(); morphia.mapPackage("com.baeldung.morphia"); Datastore datastore = morphia.createDatastore(new MongoClient(), "library"); datastore.ensureIndexes();
¡Eso es practicamente todo! Entendamos esto mejor. Necesitamos dos cosas para que funcionen nuestras operaciones de mapeo:
- Un mapeador: es responsable de mapear nuestros POJO de Java a las colecciones de MongoDB . En nuestro fragmento de código anterior, Morphia es la clase responsable de eso. Observe cómo estamos configurando el paquete donde debería buscar nuestros POJO.
- Una conexión: esta es la conexión a una base de datos MongoDB en la que el asignador puede ejecutar diferentes operaciones. La clase Datastore toma como parámetro una instancia de MongoClient (del controlador Java MongoDB) y el nombre de la base de datos MongoDB, devolviendo una conexión activa para trabajar .
Entonces, estamos listos para usar este almacén de datos y trabajar con nuestras entidades.
5. ¿Cómo trabajar con entidades?
Antes de que podamos usar nuestro almacén de datos recién creado , debemos definir algunas entidades de dominio con las que trabajar.
5.1. Entidad simple
Comencemos por definir una entidad Book simple con algunos atributos:
@Entity("Books") public class Book { @Id private String isbn; private String title; private String author; @Property("price") private double cost; // constructors, getters, setters and hashCode, equals, toString implementations }
Hay un par de cosas interesantes a tener en cuenta aquí:
- Observe la anotación @ Entity que califica este POJO para el mapeo ODM de Morphia
- Morphia, de forma predeterminada, asigna una entidad a una colección en MongoDB por el nombre de su clase, pero podemos anular esto explícitamente (como lo hemos hecho para la entidad Libro aquí)
- Morphia, de forma predeterminada, asigna las variables en una entidad a las claves en una colección MongoDB por el nombre de la variable, pero nuevamente podemos anular esto (como lo hemos hecho aquí para el costo variable )
- Por último, necesitamos marcar una variable en la entidad para que actúe como la clave principal mediante la anotación @ Id (como si estuviéramos usando ISBN para nuestro libro aquí)
5.2. Entidades con relaciones
Sin embargo, en el mundo real, las entidades no son tan simples como parecen y tienen relaciones complejas entre sí. Por ejemplo, nuestro libro de entidad simple puede tener un editor y puede hacer referencia a otros libros complementarios. ¿Cómo los modelamos?
MongoDB ofrece dos mecanismos para construir relaciones: referencias e incrustaciones . Como sugiere el nombre, con las referencias, MongoDB almacena datos relacionados como un documento separado en la misma colección o en una diferente y solo hace referencia a ellos usando su ID.
Por el contrario, con la incrustación, MongoDB almacena o más bien incrusta la relación dentro del documento principal.
Veamos cómo podemos usarlos. Comencemos por incorporar Publisher en nuestro libro :
@Embedded private Publisher publisher;
Suficientemente simple. Ahora sigamos adelante y agreguemos referencias a otros libros:
@Reference private List companionBooks;
That's it — Morphia provides convenient annotations to model relationships as supported by MongoDB. The choice of referencing vs embedding, however, should draw from data model complexity, redundancy, and consistency amongst other considerations.
The exercise is similar to normalization in relational databases.
Now, we're ready to perform some operations on Book using Datastore.
6. Some Basic Operations
Let's see how to work with some of the basic operations using Morphia.
6.1. Save
Let's begin with the simplest of the operations, creating an instance of Book in our MongoDB database library:
Publisher publisher = new Publisher(new ObjectId(), "Awsome Publisher"); Book book = new Book("9781565927186", "Learning Java", "Tom Kirkman", 3.95, publisher); Book companionBook = new Book("9789332575103", "Java Performance Companion", "Tom Kirkman", 1.95, publisher); book.addCompanionBooks(companionBook); datastore.save(companionBook); datastore.save(book);
This is enough to let Morphia create a collection in our MongoDB database, if it does not exist, and perform an upsert operation.
6.2. Query
Let's see if we're able to query the book we just created in MongoDB:
List books = datastore.createQuery(Book.class) .field("title") .contains("Learning Java") .find() .toList(); assertEquals(1, books.size()); assertEquals(book, books.get(0));
Querying a document in Morphia begins with creating a query using Datastore and then declaratively adding filters, to the delight of those in love with functional programming!
Morphia supports much more complex query construction with filters and operators. Moreover, Morphia allows for limiting, skipping, and ordering of results in the query.
What's more, Morphia allows us to use raw queries written with the Java driver for MongoDB for more control, should that be needed.
6.3. Update
Although a save operation can handle updates if the primary key matches, Morphia provides ways to selectively update documents:
Query query = datastore.createQuery(Book.class) .field("title") .contains("Learning Java"); UpdateOperations updates = datastore.createUpdateOperations(Book.class) .inc("price", 1); datastore.update(query, updates); List books = datastore.createQuery(Book.class) .field("title") .contains("Learning Java") .find() .toList(); assertEquals(4.95, books.get(0).getCost());
Here, we're building a query and an update operation to increase by one the price of all books returned by the query.
6.4. Delete
Finally, that which has been created must be deleted! Again, with Morphia, it's quite intuitive:
Query query = datastore.createQuery(Book.class) .field("title") .contains("Learning Java"); datastore.delete(query); List books = datastore.createQuery(Book.class) .field("title") .contains("Learning Java") .find() .toList(); assertEquals(0, books.size());
We create the query quite similarly as before and run the delete operation on the Datastore.
7. Advanced Usage
MongoDB has some advanced operations like Aggregation, Indexing, and many others. While it isn't possible to perform all of that using Morphia, it's certainly possible to achieve some of that. For others, sadly, we'll have to fall back to the Java driver for MongoDB.
Let's focus on some of these advanced operations that we can perform through Morphia.
7.1. Aggregation
Aggregation in MongoDB allows us to define a series of operations in a pipeline that can operate on a set of documents and produce aggregated output.
Morphia has an API to support such an aggregation pipeline.
Let's assume we wish to aggregate our library data in such a manner that we have all the books grouped by their author:
Iterator iterator = datastore.createAggregation(Book.class) .group("author", grouping("books", push("title"))) .out(Author.class);
So, how does this work? We begin by creating an aggregation pipeline using the same old Datastore. We have to provide the entity on which we wish to perform aggregation operations, for instance, Book here.
Next, we want to group documents by “author” and aggregate their “title” under a key called “books”. Finally, we're working with an ODM here. So, we have to define an entity to collect our aggregated data — in our case, it's Author.
Of course, we have to define an entity called Author with a variable called books:
@Entity public class Author { @Id private String name; private List books; // other necessary getters and setters }
This, of course, just scratches the surface of a very powerful construct provided by MongoDB and can be explored further for details.
7.2. Projection
Projection in MongoDB allows us to select only the fields we want to fetch from documents in our queries. In case document structure is complex and heavy, this can be really useful when we need only a few fields.
Let's suppose we only need to fetch books with their title in our query:
List books = datastore.createQuery(Book.class) .field("title") .contains("Learning Java") .project("title", true) .find() .toList(); assertEquals("Learning Java", books.get(0).getTitle()); assertNull(books.get(0).getAuthor());
Here, as we can see, we only get back the title in our result and not the author and other fields. We should, however, be careful in using the projected output in saving back to MongoDB. This may result in data loss!
7.3. Indexing
Indexes play a very important role in query optimization with databases — relational as well as many non-relational ones.
MongoDB defines indexes at the level of the collection with a unique index created on the primary key by default. Moreover, MongoDB allows indexes to be created on any field or sub-field within a document. We should choose to create an index on a key depending on the query we wish to create.
For instance, in our example, we may wish to create an index on the field “title” of Book as we often end up querying on it:
@Indexes({ @Index( fields = @Field("title"), options = @IndexOptions(name = "book_title") ) }) public class Book { // ... @Property private String title; // ... }
Of course, we can pass additional indexing options to tailor the nuances of the index that gets created. Note that the field should be annotated by @Property to be used in an index.
Moreover, apart from the class-level index, Morphia has an annotation to define a field-level index as well.
7.4. Schema Validation
We've got an option to provide data validation rules for a collection that MongoDB can use while performing an update or insert operation. Morphia supports this through their APIs.
Let's say that we don't want to insert a book without a valid price. We can leverage schema validation to achieve this:
@Validation("{ price : { $gt : 0 } }") public class Book { // ... @Property("price") private double cost; // ... }
There is a rich set of validations provided by MongoDB that can be employed here.
8. Alternative MongoDB ODMs
Morphia no es el único ODM de MongoDB disponible para Java. Hay varios otros que podemos considerar para usar en nuestras aplicaciones. Aquí no es posible una discusión sobre la comparación con Morphia, pero siempre es útil conocer nuestras opciones:
- Spring Data: proporciona un modelo de programación basado en Spring para trabajar con MongoDB
- MongoJack: proporciona mapeo directo de JSON a objetos MongoDB
Esta no es una lista completa de los ODM de MongoDB para Java, ¡pero hay algunas alternativas interesantes disponibles!
9. Conclusión
En este artículo, entendimos los detalles básicos de MongoDB y el uso de un ODM para conectar y operar en MongoDB desde un lenguaje de programación como Java. Exploramos aún más Morphia como un ODM de MongoDB para Java y las diversas capacidades que tiene.
Como siempre, el código se puede encontrar en GitHub.