Domain-model persistence with Morphia and MongoDB

Use Morphia to persist, load, delete, and query a Java domain model mapped to MongoDB

Morphia is a type-safe, object-mapping library for MongoDB, an open source document-oriented database. This article explains the benefits of mapping documents to and from objects and shows how to use Morphia for this purpose. Then it demonstrates how to persist, load, delete, and query a Java™ domain model mapped to MongoDB.

John D'Emic, Senior Software Engineer, IBM

ohn D'EmicJohn D'Emic is a Senior Software Engineer with IBM Global Services and has been using MongoDB in a variety of development contexts over the last year. He is a co-author (with David Dossot) of Mule in Action (Manning Publications, 2009).



25 January 2011

Also available in Chinese Japanese

MongoDB is a document-oriented database for storing and retrieving JavaScript Object Notation (JSON)-style documents. Augmented by indexing, replication, and sharding capabilities, MongoDB has emerged as a robust and scalable NoSQL contender (see Resources).

Video demo: An introduction to MongoDB

This demo shows introduces MongoDB, shows you how it works and explains in which doman models it is most useful.

An official Java driver is available for interacting with MongoDB. The driver provides a Map implementation, BasicDBObject, for representing documents in the datastore. Although the Map representation is convenient, particularly when serializing back and forth from JSON, being able to represent the documents as a Java class hierarchy also has its advantages. Mapping documents back and forth from a Java domain model, for instance, allows you to enforce type safety on the Java layer while enjoying the benefits of schema-free development with MongoDB. And many Java frameworks assume the use of POJOs (plain old Java objects), or are more capable of handling them.

Morphia is an Apache-licensed Google Code project that lets you persist, retrieve, delete, and query POJOs stored as documents in MongoDB. Morphia accomplishes this by providing a set of annotations and a wrapper around the Mongo Java driver. Morphia is conceptually similar to object-relational mappers such as Java Persistence API (JPA) or Java Data Objects (JDO) implementations. In this article, I'll show how to use Morphia with a Java domain model mapped to MongoDB. See Download to get the complete sample code.

Defining the domain model

I'll use a simplified domain model to demonstrate Morphia's functionality. BandManager, a hypothetical web application, supplies data about musical acts: their members, distributor, back catalog, genre, and so on. I'll define Band, Song, Distributor, and ContactInfo classes to represent this domain model, as shown in Figure 1:

Figure 1. BandManager's classes
UML diagram of the domain-model class hierarchy for the BandManager application

The Unified Modeling Language (UML) diagram in Figure 1 shows the domain-model class hierarchy. The rectangle on the left represents the Band class. Stacked on the right are rectangles representing the ContactInfo, Distributor, and Song classes, respectively. An arrow leading from Band to ContactInfo is marked with a 1 on the ContactInfo side, indicating a one-to-one relationship between the two classes. A line connecting Band to Distributor is marked on the Band side with 0..* and with a 1 on the Distributor side, indicating that a Band has a single Distributor and that a Distributor represents many Bands. Finally, an arrow from Band to Song is marked on the Song side with catalog 0..1, indicating that the Band has a one-to-many relationship with Song and that this relationship is called a catalog.

I'll annotate these classes and then use Morphia's Datastore interface to save them as documents in MongoDB.

Annotating the domain model

Listing 1 shows how the Band class is annotated:

Listing 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;

The @Entity annotation is required. It declares that the class is to be persisted as a document in a dedicated collection. The value supplied to the @Entity annotation, bands, defines how the collection is named. Morphia, by default, uses the class name to name the collection. If I left out the bands value, for instance, the collection would be called Band in the database.

Datatypes

MongoDB supports a smaller set of datatypes than the Java language, namely integer, long, double, and string. Morphia converts basic Java types (such as float) automatically for you.

The @Id annotation instructs Morphia which field to use as the document ID. If you try to persist an object whose @Id annotated field is null, then Morphia autogenerates an ID value for you.

Morphia tries to persist any unannotated fields it encounters, unless they are marked with the @Transient annotation. The name and genre properties, for example, will be saved as strings in the document, keyed with name and genre.

The distributor, songs, members, and info properties reference other objects. A member object, unless annotated with @Reference as you'll see shortly, is assumed to be embedded. It will appear as a child in the parent document in the collection. For example, the members List would look like this when persisted:

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

The info property is another embedded object. In this case, I am explicitly setting the @Embedded annotation with a value of info. This overrides the default naming of the child in the document, which would otherwise be called contactInfo. For example:

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

@Reference and DBRefs

Under the covers, Morphia uses a Mongo DBRef to reference objects in a different collection.

Using the @Reference annotation indicates the object is a reference to a document in another collection. When the object is loaded from the Mongo collection, Morphia follows these references to build the object graph. For example, the distributor property looks like this in the persisted document:

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

As with the @Embedded annotation, @Reference can take a value to override the default naming. In this case, I'm calling the List of songs a catalog in the document.

Now take a look at the class definitions for Song, Distributor, and ContactInfo. Listing 2 shows the definition for Song:

Listing 2. Song.java
@Entity("songs")
public class Song {

    @Id
    ObjectId id;

    String name;

Listing 3 shows the definition for Distributor:

Listing 3. Distributor.java
@Entity("distributors")
public class Distributor {

    @Id
    ObjectId id;

    String name;

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

Listing 4 shows the definition for ContactInfo:

Listing 4. ContactInfo.java
public class ContactInfo {


    public ContactInfo() {
    }

    String city;

    String phoneNumber;

The ContactInfo class lacks an @Entity annotation. This is deliberate, because I don't need a dedicated collection for ContactInfo. Instances will always be embedded in band documents.

Now that I've defined and annotated the domain model, I'll show you how to use Morphia's Datastore to save, load, and delete entities.


Using the Datastore

Dependency injection (DI)

Datastore and Mongo are both DI-friendly. You shouldn't have any trouble wiring them up in either Spring or Guice, for instance. If possible, you should configure each as a singleton and share them amongst collaborating beans.

The Datastore interface — a wrapper around the Mongo Java driver — is used to manage entities in MongoDB. Because the Datastore requires a Mongo instance for instantiation, you can reuse an existing Mongo instance or configure one appropriately for your environment. Here's an example of instantiating a Datastore that connects to a local MongoDB instance:

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

Next I'll create an instance of 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");

Now that I have a Band instance, I can use datastore to persist it:

datastore.save(band);

The band should now be saved in a collection called bands in the bandmanager db. Using the Mongo command-line interface client, I can take a look to make sure (lines are broken in this and other examples to fit this article's page width):

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

Nice! It's in there. Everything looks as you'd expect except for the className field. Morphia automatically creates this field to record the type of the object in MongoDB. It's primarily used to determine the types of the objects not necessarily known at compile time (when you load objects from a collection with mixed types, for example). If this bothers you and you know you won't need the functionality, you can disable the className from being persisted by adding the noClassnameStored value to the @Entity annotation:

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

Now I'll load the Band and assert that it's equal to the band I persisted:

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

The get() method of Datastore allows you to load an entity using its ID. You don't need to specify the collection or define a query string to load the object. You just tell the Datastore which class you want to load and what its ID is. Morphia does the rest.

Now it's time to look at the collaborating objects for a Band. I'll start by defining some Songs, then add them to the Band instance I just created:

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

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

If I check the songs collection in Mongo, I should see something like this:

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

Note that the Songs aren't referenced from the band yet. I'll add them to the band and see what happens:

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

datastore.save(band);

Now when I query the bands collection, I should see this:

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

Transactions

It's important to remember that MongoDB does not support transactions as most relational database management systems do. If your application needs to coordinate multiple threads writing to or reading from a collection, you must rely on the Java language's serialization and concurrency features.

Note how the songs collection is saved as an array called catalog as two DBRefs.

A current limitation is that referenced objects must be saved before other objects can reference them. This is why I saved song1 and song2 before adding them to the band.

Now I'll delete song2:

datastore.delete(song2);

Querying the songs collection should indicate that song2 is missing. But if you look at the band, you'll see that the song is still there. Even worse, trying to load the band entity causes an exception:

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

For now, to avoid this error, you need to remove references to a song manually before deleting it.


Querying

Loading entities by their IDs will only get me so far. Ultimately I want to be able to query Mongo for the entities I want.

Instead of loading a band by its ID, I'll query for it by name. I do this by creating a Query object and specifying a filter to get the results I want:

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

I specify the class I want to query for, Band, and a filter to the createQuery() method. Once I've defined the query, I can use the asList() method to access the results:

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

Morphia's filter operators closely map to the query operators used in MongoDB queries. The = operator I used in the above query, for instance, is analogous to the $eq operator in MongoDB. Full details on the filter operators are available in the Morphia online documentation (see Resources).

As an alternative to filter queries, Morphia offers a fluent interface for building queries. The following fluent-interface query, for example, is identical to the preceding filter query:

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

You can use "dot notation" to query embedded objects. Here's a query that uses dot notation and the fluent interface to select all bands based in Brooklyn:

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

You can further define the query's result set. I'll modify the previous query to sort the bands by name and limit the results to 100:

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

Indexing

You'll notice as your collections grow that query performance will degrade. Mongo collections, much like relational database tables, need to be properly indexed to ensure reasonable query performance.

Annotating a property with the @Indexed annotation applies an index to the field. Here I create an ascending index called genreName on the genre property of a Band:

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

To apply the indexes, Morphia needs to know which classes are going to be mapped. You need to instantiate Morphia a little bit differently to ensure the indexes are applied. You can do this as follows:

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

The final ensureIndexes() call instructs the datastore to create the required indexes if they don't already exist.

Indexes can also be used to prevent duplicates from being inserted into a collection. By setting the unique property on the @Indexed annotation for a band's name, for example, I can ensure that only one band with the given name is in the collection:

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

Subsequent same-named bands would be dropped.


In conclusion

Morphia is a powerful tool for interacting with MongoDB. It allows type-safe, idiomatic access to MongoDB documents. This article covered the primary aspects of working with Morphia but excluded some features. I encourage you to check out the Morphia Google Code project for information about its data access object (DAO) support, validation, and manual mapping capabilities.


Download

DescriptionNameSize
Sample code for this articlej-morphia.zip17.2KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology, Open source
ArticleID=617872
ArticleTitle=Domain-model persistence with Morphia and MongoDB
publish-date=01252011