Introduzione a Morphia - Java ODM per MongoDB

1. Panoramica

In questo tutorial, capiremo come utilizzare Morphia, un Object Document Mapper (ODM) per MongoDB in Java.

Nel processo, capiremo anche cos'è un ODM e come facilita il lavoro con MongoDB.

2. Cos'è un ODM ?

Per chi non lo sapesse in quest'area, MongoDB è un database orientato ai documenti costruito per essere distribuito dalla natura . I database orientati ai documenti, in termini semplici, gestiscono i documenti, che non sono altro che un modo senza schema di organizzare i dati semi-strutturati . Rientrano in un ombrello più ampio e vagamente definito di database NoSQL, dal nome del loro apparente allontanamento dall'organizzazione tradizionale dei database SQL.

MongoDB fornisce driver per quasi tutti i linguaggi di programmazione più diffusi come Java . Questi driver offrono un livello di astrazione per lavorare con MongoDB in modo che non lavoriamo direttamente con Wire Protocol. Pensa a questo come a Oracle che fornisce un'implementazione del driver JDBC per il loro database relazionale.

Tuttavia, se ricordiamo i nostri giorni in cui lavoravamo direttamente con JDBC, possiamo apprezzare quanto possa essere complicato, specialmente in un paradigma orientato agli oggetti. Fortunatamente, abbiamo framework ORM (Object Relational Mapping) come Hibernate in nostro soccorso. Non è molto diverso per MongoDB.

Sebbene possiamo certamente lavorare con il driver di basso livello, richiede molto più boilerplate per eseguire l'operazione. Qui, abbiamo un concetto simile a ORM chiamato Object Document Mapper (ODM) . Morphia riempie esattamente quello spazio per il linguaggio di programmazione Java e funziona sopra il driver Java per MongoDB.

3. Impostazione delle dipendenze

Abbiamo visto abbastanza teoria per introdurci in un codice. Per i nostri esempi, modelleremo una libreria di libri e vedremo come gestirla in MongoDB usando Morphia.

Ma prima di iniziare, avremo bisogno di impostare alcune delle dipendenze.

3.1. MongoDB

Abbiamo bisogno di un'istanza in esecuzione di MongoDB con cui lavorare. Esistono diversi modi per ottenerlo e il più semplice è scaricare e installare l'edizione della community sul nostro computer locale.

Dovremmo lasciare tutte le configurazioni predefinite così come sono, inclusa la porta su cui viene eseguito MongoDB.

3.2. Morfina

Possiamo scaricare i JAR predefiniti per Morphia da Maven Central e utilizzarli nel nostro progetto Java.

Tuttavia, il modo più semplice è utilizzare uno strumento di gestione delle dipendenze come Maven:

 dev.morphia.morphia core 1.5.3 

4. Come connettersi usando Morphia?

Ora che abbiamo MongoDB installato e funzionante e abbiamo configurato Morphia nel nostro progetto Java, siamo pronti per connetterci a MongoDB usando Morphia.

Vediamo come possiamo farlo:

Morphia morphia = new Morphia(); morphia.mapPackage("com.baeldung.morphia"); Datastore datastore = morphia.createDatastore(new MongoClient(), "library"); datastore.ensureIndexes();

Questo è praticamente tutto! Capiamolo meglio. Abbiamo bisogno di due cose per far funzionare le nostre operazioni di mappatura:

  1. Un mappatore: è responsabile della mappatura dei nostri POJO Java sulle raccolte MongoDB . Nel nostro frammento di codice sopra, Morphia è la classe responsabile di ciò. Nota come stiamo configurando il pacchetto in cui dovrebbe cercare i nostri POJO.
  2. Una connessione: questa è la connessione a un database MongoDB su cui il mappatore può eseguire diverse operazioni. La classe Datastore prende come parametro un'istanza di MongoClient (dal driver Java MongoDB) e il nome del database MongoDB, restituendo una connessione attiva con cui lavorare .

Quindi, siamo tutti pronti per utilizzare questo Datastore e lavorare con le nostre entità.

5. Come lavorare con le entità?

Prima di poter utilizzare il nostro Datastore appena coniato , dobbiamo definire alcune entità di dominio con cui lavorare.

5.1. Entità semplice

Cominciamo definendo una semplice entità Book con alcuni attributi:

@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 }

Ci sono un paio di cose interessanti da notare qui:

  • Notare l'annotazione @ Entity che qualifica questo POJO per la mappatura ODM di Morphia
  • Morphia, per impostazione predefinita, mappa un'entità a una raccolta in MongoDB con il nome della sua classe, ma possiamo sovrascriverla esplicitamente (come abbiamo fatto per l'entità Book qui)
  • Morphia, per impostazione predefinita, mappa le variabili in un'entità alle chiavi in ​​una raccolta MongoDB in base al nome della variabile, ma ancora una volta possiamo sovrascriverlo (come abbiamo fatto per il costo della variabile qui)
  • Infine, dobbiamo contrassegnare una variabile nell'entità in modo che agisca come chiave primaria dall'annotazione @ Id (come se stessimo usando ISBN per il nostro libro qui)

5.2. Entità con relazioni

Nel mondo reale, tuttavia, le entità non sono così semplici come sembrano e hanno relazioni complesse tra loro. Ad esempio, la nostra entità semplice Libro può avere un editore e può fare riferimento ad altri libri associati. Come li modelliamo?

MongoDB offre due meccanismi per creare relazioni: riferimenti e incorporamento . Come suggerisce il nome, con i riferimenti, MongoDB memorizza i dati correlati come documento separato nella stessa raccolta o in una raccolta diversa e vi fa riferimento semplicemente usando il suo ID.

Al contrario, con l'incorporamento, MongoDB memorizza o piuttosto incorpora la relazione all'interno del documento genitore stesso.

Vediamo come possiamo usarli. Cominciamo incorporando Publisher nel nostro libro :

@Embedded private Publisher publisher;

Abbastanza semplice. Ora andiamo avanti e aggiungiamo riferimenti ad altri libri:

@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 non è l'unico ODM MongoDB disponibile per Java. Ce ne sono molti altri che possiamo considerare di utilizzare nelle nostre applicazioni. Una discussione sul confronto con Morphia non è possibile qui, ma è sempre utile conoscere le nostre opzioni:

  • Spring Data: fornisce un modello di programmazione basato su Spring per lavorare con MongoDB
  • MongoJack: fornisce il mapping diretto da JSON a oggetti MongoDB

Questo non è un elenco completo di MongoDB ODM per Java, ma sono disponibili alcune alternative interessanti!

9. Conclusione

In questo articolo, abbiamo compreso i dettagli di base di MongoDB e l'uso di un ODM per connettersi e operare su MongoDB da un linguaggio di programmazione come Java. Abbiamo ulteriormente esplorato Morphia come MongoDB ODM per Java e le varie funzionalità che ha.

Come sempre, il codice può essere trovato su GitHub.