It is wonderful that Google provides JDO and JPA interfaces to the AppEngine Datastore. However, the learning curve is somewhat steep due to AppEngine’s non-relational nature and unique concepts such as entity groups, owned and un-owned relationships, etc. In addition, the PersistenceManager lifecycle (and particularly the necessity of detaching objects) often gets in the way. I’ve written a lot of code on this blog just to handle injecting the PM where it’s needed in services, unit tests, etc.
In stark contrast to all this stands the DataStore low-level API. You can call the DatastoreService from anywhere in your code via the DatastoreServiceFactory, and there are only four methods to understand: get, put, delete, and prepare (query). It makes no pretense of being relational, but unfortunately, no pretense of being an object store, either, as the low-level API stores only objects of type com.google.appengine.api.datastore.Entity.
Enter Objectify, a recently-announced open source persistence framework for AppEngine. Objectify preserves the simplicity and transparency of the low-level API and does all the work of converting your domain objects to and from Datastore Entities. It’s a tiny jar (36k) with no external dependencies and zero startup time, which helps mitigate the AppEngine cold start problem.
In the process of Objectify-ing my old JDO code, I’m building a base DAO class that I can simply extend for each domain class and get all the standard CRUD operations for free. Because most Objectify methods use parameterized types, my generic DAO is largely redundant, but nevertheless provides a consistent layer in which to add type-specific methods like findUserByEmailOrPhone. Let’s look at some code.
First, a simple domain class. Note there are fewer and simpler annotations required than for JPA / JDO.
package com.roa.common.domain; import java.io.Serializable; import javax.persistence.Entity; import javax.persistence.Id; @Entity public class User implements Serializable { private static final long serialVersionUID = -1126191336687818754L; @Id // Objectify auto-generates Long IDs just like JDO / JPA private Long id; private String firstName; private String lastName; private String emailAddress; private String zipCode; private String googleAccountId; public User() { // Empty constructor needed for GWT serialization and Objectify } public Long getId() { return id; } public void setId(Long id) { this.id = id; } ... }
We can do persistence operations by calling the UserDao:
// Create new user User u = new User(); u.setEmailAddress("test@example.com"); u.setFirstName("Test"); u.setLastName("User"); u.setGoogleAccountId("testAccountId"); UserDao userDao = new UserDao(); OKey<User> key = userDao.add(u); // Retrieve user by ID dao.get(u.getId()); // Find users matching an email address users = dao.listByProperty("emailAddress", "test@example.com");
All the DAO methods above are provided in my base DAO, so the UserDao itself is quite simple:
package com.roa.server.dao; import java.util.logging.Logger; import com.googlecode.objectify.ObjectifyService; import com.roa.common.domain.User; public class UserDao extends ObjectifyDao<User> { private static final Logger LOG = Logger.getLogger(UserDao.class.getName()); static { ObjectifyService.register(User.class); } public UserDao() { super(User.class); } }
And finally, my generic base DAO (a work in progress, caveat emptor). Note that I’ve created a couple methods to allow query by example, which I generally prefer to enumerating methods for every use case (getByEmail, getByLastName, getByFirstAndLastName, etc.). The getByProperty and listByProperty methods take a single property name and value on which to filter, and getByExample / listByExample allow you to specify multiple property names on which to filter.
package com.roa.server.dao; import java.beans.BeanInfo; import java.beans.IntrospectionException; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.List; import com.google.appengine.api.datastore.EntityNotFoundException; import com.googlecode.objectify.OKey; import com.googlecode.objectify.OQuery; import com.googlecode.objectify.ObjectifyService; import com.googlecode.objectify.helper.DAOBase; public class ObjectifyDao<T> extends DAOBase { private Class<T> clazz; /** * We've got to get the associated domain class somehow * * @param clazz */ protected ObjectifyDao(Class<T> clazz) { this.clazz = clazz; } public OKey<T> add(T entity) { OKey<T> key = ofy().put(entity); return key; } public void delete(T entity) { ofy().delete(entity); } public void delete(OKey<T> entityKey) { ofy().delete(entityKey); } public T get(Long id) throws EntityNotFoundException { T obj = ofy().get(this.clazz, id); return obj; } /** * Convenience method to get an object matching a single property * * @param propName * @param propValue * @return T matching Object */ public T getByProperty(String propName, Object propValue) { OQuery<T> q = ObjectifyService.createQuery(clazz); q.filter(propName, propValue); T obj = ofy().prepare(q).asSingle(); return obj; } public List<T> listByProperty(String propName, Object propValue) { OQuery<T> q = ObjectifyService.createQuery(clazz); q.filter(propName, propValue); List<T> list = ofy().prepare(q).asList(); return list; } public T getByExample(T u, String... matchProperties) { OQuery<T> q = ObjectifyService.createQuery(clazz); // Find non-null properties and add to query for (String propName : matchProperties) { Object propValue = getPropertyValue(u, propName); q.filter(propName, propValue); } T obj = ofy().prepare(q).asSingle(); return obj; } public List<T> listByExample(T u, String... matchProperties) { OQuery<T> q = ObjectifyService.createQuery(clazz); // Find non-null properties and add to query for (String propName : matchProperties) { Object propValue = getPropertyValue(u, propName); q.filter(propName, propValue); } List<T> list = ofy().prepare(q).asList(); return list; } private Object getPropertyValue(Object obj, String propertyName) { BeanInfo beanInfo; try { beanInfo = Introspector.getBeanInfo(obj.getClass()); } catch (IntrospectionException e) { throw new RuntimeException(e); } PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); for (PropertyDescriptor propertyDescriptor : propertyDescriptors) { String propName = propertyDescriptor.getName(); if (propName.equals(propertyName)) { Method readMethod = propertyDescriptor.getReadMethod(); try { Object value = readMethod.invoke(obj, new Object[] {}); return value; } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } } } return null; } }
Have fun!