Search This Blog

Friday, October 1, 2010

MongoDB with Morphia - An example

MongoDB is an open source highly scalable, performant document oriented database and I wanted to play with the same. The database itself is feature rich offering features such as sharding, in place updates and map reduce.

The database is written in C++ and uses JSON style documents for mapping objects. Mongo's java API provides the concept of an object that takes name-value pair's depicting the data and then stores the same. There is a lack of type safety with this approach and also the effort of converting regular java pojo's into the downstream mongo object.

The type safety issue is addressed by the Morphia project that allows for easy mapping of objects from-to MongoDB while also providing a querying interface. The API itself makes use of annotations thus not requiring the use of any configuration files. Think of this like Hibernate/JPA with annotations for Mongo.

The API itself provides for access to Mongo directly if required. In this BLOG, I am trying out a simple example of using Morphia. I developed the project in the following steps:

1. Connection to mongo
2. POJO or Model
3. DAO

I have used a simple data model of an Order and its ancillary objects.

1. Connection to Mongo:
The Mongo object itself is a connection pool so one does not need to create an additional one. Take a look at the documentation on the same.

I define a simple Connection manager that is a singleton that handles the initialization of a Morphia DataStore instance as shown below:
public final class MongoConnectionManager {
  private static final MongoConnectionManager INSTANCE = new MongoConnectionManager();

  private final Datastore db;
  public static final String DB_NAME = "mydb";
  
  private MongoConnectionManager() {
    try {
      Mongo m = new Mongo("localhost", 27017);
      db = new Morphia().map(Order.class).map(LineItem.class).map(Customer.class).createDatastore(
        m, DB_NAME);
      db.ensureIndexes();
    }
    catch (Exception e) {
      throw new RuntimeException("Error initializing mongo db", e);
    }
  }

  public static MongoConnectionManager instance() {
    return INSTANCE;
  }
  ...
}

Also note that in the above code, there is a call to db.ensureIndexes(). This method will synchronously create indices if not present and if present continues on seamlessly.

2. Model:
I then defined my Order Model as shown below:
@Entity(value="orders", noClassNameStored = true)
public class Order {
  @Id
  private ObjectId id;
  @Reference
  private Customer customer;
  @Embedded
  private List<LineItem> lines;
    
  private Date creationDate;
  private Date lastUpdateDate;
  ...
  // Getters and setters
  ..
  
  @PrePersist
  public void prePersist() {
    this.creationDate = (creationDate == null) ? new Date() : creationDate;
    this.lastUpdateDate = (lastUpdateDate == null) ? creationDate : new Date();
  }
}
The morphia annotations are not JPA but very similar. As shown above we are mapping the Order class to the mongo collection of "orders", we define an Id annotation indicating the order identifier. As the type of the Id has been defined as ObjectId, Mongo will generate the Id automatically. If one uses any other type, the Id must be explicitly set. Also note the explicit setting of noClassNameStored as the class name will be otherwise be stored by default. The storing of the class name becomes useful when working with multiple-inheritance structures where the correct class would need to be instantiated.

Note that the Customer object has been defined with an @Reference annotation indicating that the Customer object can exist independent of the Order object and an existing one must be provided before an order can be persisted.

The @Embedded tag on the Line item indicates that the line items lie in the scope of an order and will not exist independently without an order.

The create and update dates have not been annotated but will be included in the MongoDB collection automatically. One could alternatively add the @Property tag to the same and provide a specific name under which the property would reside.

If there is a field that one would not want persisted, then marking the same with @Transient will prevent the same.

Also note the segment where the prePersist methog tagged with the @PrePersist annotation is used to set the dates on the order prior to it being saved. Equivalent annotations exist for @PostPersist, @PreLoad and @PostLoad. The framework also supports the concept of EntityListeners for life cycle phases. So one can create an external class that responds to different
life cycle events as so:
@EntityListeners(OrderListener.class)
public class Order {
}

public class OrderListener {
  @PrePersist
  public void preCreate(Order order) {
    ...
  }
}

Viewing the order using the MongoDB console would display an order as such:
db.orders.find().forEach(printjson);
{
 "_id" : ObjectId("4ca4e5dbb95a4d64192cf119"),
 "customer" : {
  "$ref" : "customers",
  "$id" : ObjectId("4ca4e5dbb95a4d64172cf119")
 },
 "lines" : [
  {
   "lineNumber" : 1,
   "quantity" : 10,
   "product" : {
    "$ref" : "products",
    "$id" : "xbox"
   }
  }
 ],
 "creationDate" : "Thu Sep 30 2010 19:32:43",
 "lastUpdateDate" : "Thu Sep 30 2010 19:32:43"
}

3. DAO:
The Morphia framework provides a DAO class. Nice. The DAO class provides type safe DAO behavior. So if one has a customer object as an example:

DAO<Customer, String> customerDao = new DAO<Customer, String>(Customer.class, dataSource);

// save
customerDao.save(new Customer());
// Read
Customer sanjay = customerDao.createQuery().field("lastName").equal("acharya").get();

The DAO class however provides many operations that might not be desired such as dropCollection(). Decorating the same or extending the same is an easy enough procedure to limit the access of some of these methods while providing additional ones.

public class OrderDaoImpl extends DAO<Order, ObjectId> implements OrderDao {
  public OrderDaoImpl() {
    super(Order.class, MongoConnectionManager.instance().getDb());
  }
  ....

  @Override
  public List<Order> findOrdersByCustomer(Customer customer) {
   return createQuery().field("customer").equal(customer).asList();
  }

  @Override
  public List<Order> findOrdersWithProduct(Product product) {
    return createQuery().field("lines.product").equal(product).asList();
  }
}

From the above code, you see that the query API from Morphia provides an abstraction over the raw json query of mongo. Type safety is present but as in all cases for no fault of theirs, field naming will not be type safe. The "." notation is used to access nested fields,

Another example of a query involving multiple fields is shown below:
public List<Product> getProductsWithCategory(CategoryType type, double maxPrice) {
    return createQuery().field("price").lessThanOrEq(maxPrice).field("categoryType").equal(type).asList();
  }

Indexes can be applied to fields via the @Indexed annotation. For example,in the code shown below, the lastName property of a Customer is to be indexed in decending order.
@Indexed(value=IndexDirection.DESC, name="lastNameIndex", dropDups=false) 
  private String lastName;

I found morphia pretty easy to use with mongo. It will be nice if they support the JPA annotations. There are still things I want to try such as sharding and maybe Lucene integration for full text based search.

The example provided demonstrates Morphia in action with tests that use the model objects and DAOs. To get the tests running, download MongoDB , install the same and have it running on the default port. Run "maven test" in the project provided to see the tests or import the project for a code review :-)

The example can be downloaded HERE....

Some links:
1. A presentation on Mongo and Java
2. 10 things about No-sql databases