Morphia and MongoDB: Evolving Document Structures
To follow up on that post, I’m going to discuss how to deal with some real life needs: handling changing schemas and customizing your mapping to handle things like read-only fields and replacing simple fields with complex objects.
Changing Schemas
As nearly anyone who has worked with databases in the development world knows, schemas are always evolving. Fields get deprecated or outright dropped, tables become obsolete, new fields are added, and so on.
While a lot of this pain is avoided by using a schemaless datastore like MongoDB, sometimes we still do need special handling for changes, and in the case of Morphia, we essentially have defined a schema, so we do have to find ways to deal with this. The nice part about it is that Morphia makes it very clean and easier than you’ll see in just about in any ORM.
Deprecating Fields
One good example is a deprecated field that has been replaced by another field. Let’s imagine you have a bug tracking system with documents that look something like this:
{ _id:1, desc: "IE Rendering broken on intranet site", componentName: "INTRANET", dateCreated: ISODate("2011-09-06T20:52:50.258Z") }
Here is the Morphia definition:
@Entity("issues") class Issue { @Id private long id; private String desc; private String componentName; private Date dateCreated = new Date(); }
Now imagine at some point we decide to do away with the component field and make it a more generic free text field where users can enter multiple components, versions, or other helpful information. We don’t want to just stick that in the component field, as that would lead to confusion.
Thankfully, we have a something in the Morphia toolkit that is made exactly for this – The @AlsoLoad annotation. This annotation allows us to populate a POJO field with one of multiple possible sources. We simply update our Morphia mapping to indicate an old field name, and we can easily remove references to the old field without breaking anything. This keeps our code and documents clean.
@Entity("issues") class Issue { @Id private long id; private String desc; @AlsoLoad("componentName") // handle old componentName field private String affects; private Date dateCreated = new Date(); }
So here we’ve defined automatic translation of our old field without any need to update documents or write special logic within our POJO class to handle documents differently depending on when they were created.
One important note: in this example, if both the affects field and the old componentName field exist, Morphia will throw an exception, so don’t try using this for anything other than deprecating fields, or perhaps populating a single field with two mutually exclusive properties.
Supporting Read-Only for Deprecated Fields
Another possibility is that you just have to support an old field in document that the application no longer writes. This is a very simple one: use the @NotSaved annotation. When you use this on a field, the data will be loaded but not written by Morphia.
In our previous example, we could just as easily have decided to just support display for the old field but not treat populate it into the affects field, so let’s alter our Morphia POJO a bit to show how @NotSaved is used.
@Entity("issues") class Issue { @Id private long id; private String desc; private String affects; @NotSaved("componentName") // load old componentName field for display only private String componentName private Date dateCreated = new Date(); }
Replacing a Field with An Embedded Object
Now what if our componentName field had actually changed to a complex component object which has a name, version and build number? This is a bit trickier since we want to replace one field with multiple. We can’t attempt to load the field from multiple sources since they have different structures. Of course, we can use an embedded object to store the complex component information, but how can we make our code work seamlessly either way without having to update our documents?
In this case, the simplest approach would be to use a combination of three annotations. First we would mark the old field with the @NotSaved annotation, introduce a new embedded Component object using the @Embedded annotation, and finally take advantage one more annotation that Morphia provides – @PostLoad. This one lets us have a method that is executed after the POJO is populated from MongoDB.
Here’s the example:
@Entity("issues") class Issue { @Id private long id; private String desc; private String affects; @NotSaved("componentName") // load old componentName to convert to component private String componentName @Embedded // our new complex Component private Component component; private Date dateCreated = new Date(); // getters and setters ... @PostLoad protected void handleComponent() { if (component == null && componentName != null) { component = new Component(componentName, null, null); } } } class Component { private String componentName; private Long version; private Long buildNumber; public Component(String componentName, Long version, Long buildNumber) { // ... } // getters and setters ... }
In this case, we could remove the getter and setter for the componentName field, so that our mapped object only exposes the new and improved interface.
Conclusion
By using the powerful tools that Morphia gives us through its annotation support, we can meet these goals:
- Let our document structure adapt with the application and stay clean.
- Seamlessly handle changing structure in our Java code without error-prone code.
- Expose only the new schema while supporting the old (truly obsolete the old code and fields.
Hopefully this helps a few of you out with adapting to evolving documents, or at least to become more familiar with the abilities some of these Morphia annotations give you.
References: Morphia and MongoDB: Evolving Document Structures from our JCG partner Craig Flichel at the Carfey Software Blog