TurboManage

David Chandler's Journal of Java Web and Mobile Development

  • David M. Chandler


    Web app developer since 1994 and Google Cloud Platform Instructor now residing in Colorado. Besides tech, I enjoy landscape photography and share my work at ColoradoPhoto.gallery.

  • Subscribe

  • Enter your email address to subscribe to this blog and receive notifications of new posts by email.

    Join 231 other followers

  • Sleepless Nights…

    January 2010
    S M T W T F S
    « Dec   Feb »
     12
    3456789
    10111213141516
    17181920212223
    24252627282930
    31  
  • Blog Stats

    • 998,375 hits

Archive for January 28th, 2010

Simplify with Objectify: a lightweight persistence framework for AppEngine

Posted by David Chandler on January 28, 2010

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!

Advertisements

Posted in AppEngine | 23 Comments »

 
%d bloggers like this: