Generic DAO for Objectify 2
Posted by David Chandler on February 9, 2010
Objectify 2 RC1 is available and I’ve updated the generic DAO I wrote about previously to use the improved Query interface. In addition, I’ve rewritten the query-by-example methods to filter on all non-null properties of the example object (but it ignores many types–see comments). Finally, note that Objectify 2 Query no longer passes through AppEngine’s TooManyResultsException if you try to do a get() which returns multiple entities. I liked this behavior, so I’ve added it back in the getByExample() method below for illustration. Enjoy.
package sandbox.server.dao; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import javax.persistence.Embedded; import javax.persistence.Transient; import com.google.appengine.api.datastore.EntityNotFoundException; import com.googlecode.objectify.Key; import com.googlecode.objectify.Query; import com.googlecode.objectify.helper.DAOBase; public class ObjectifyGenericDao<T> extends DAOBase { static final int BAD_MODIFIERS = Modifier.FINAL | Modifier.STATIC | Modifier.TRANSIENT; static { // Register all your entity classes here // ObjectifyService.register(MyDomain.class); // ... } protected Class<T> clazz; /** * We've got to get the associated domain class somehow * * @param clazz */ protected ObjectifyGenericDao(Class<T> clazz) { this.clazz = clazz; } public Key<T> put(T entity) { return ofy().put(entity); } public List<Key<T>> putAll(Iterable<T> entities) { return ofy().put(entities); } public void delete(T entity) { ofy().delete(entity); } public void deleteKey(Key<T> entityKey) { ofy().delete(entityKey); } public void deleteAll(Iterable<T> entities) { ofy().delete(entities); } public void deleteKeys(Iterable<Key<T>> keys) { ofy().delete(keys); } public T get(Long id) throws EntityNotFoundException { return ofy().get(this.clazz, id); } public T get(Key<T> key) throws EntityNotFoundException { return ofy().get(key); } /** * Convenience method to get all objects matching a single property * * @param propName * @param propValue * @return T matching Object */ public T getByProperty(String propName, Object propValue) { Query<T> q = ofy().query(clazz); q.filter(propName, propValue); return q.get(); } public List<T> listByProperty(String propName, Object propValue) { Query<T> q = ofy().query(clazz); q.filter(propName, propValue); return asList(q.fetch()); } public List<Key<T>> listKeysByProperty(String propName, Object propValue) { Query<T> q = ofy().query(clazz); q.filter(propName, propValue); return asKeyList(q.fetchKeys()); } public T getByExample(T exampleObj) { Query<T> queryByExample = buildQueryByExample(exampleObj); Iterable<T> iterableResults = queryByExample.fetch(); Iterator<T> i = iterableResults.iterator(); T obj = i.next(); if (i.hasNext()) throw new RuntimeException("Too many results"); return obj; } public List<T> listByExample(T exampleObj) { Query<T> queryByExample = buildQueryByExample(exampleObj); return asList(queryByExample.fetch()); } private List<T> asList(Iterable<T> iterable) { ArrayList<T> list = new ArrayList<T>(); for (T t : iterable) { list.add(t); } return list; } private List<Key<T>> asKeyList(Iterable<Key<T>> iterableKeys) { ArrayList<Key<T>> keys = new ArrayList<Key<T>>(); for (Key<T> key : iterableKeys) { keys.add(key); } return keys; } private Query<T> buildQueryByExample(T exampleObj) { Query<T> q = ofy().query(clazz); // Add all non-null properties to query filter for (Field field : clazz.getDeclaredFields()) { // Ignore transient, embedded, array, and collection properties if (field.isAnnotationPresent(Transient.class) || (field.isAnnotationPresent(Embedded.class)) || (field.getType().isArray()) || (Collection.class.isAssignableFrom(field.getType())) || ((field.getModifiers() & BAD_MODIFIERS) != 0)) continue; field.setAccessible(true); Object value; try { value = field.get(exampleObj); } catch (IllegalArgumentException e) { throw new RuntimeException(e); } catch (IllegalAccessException e) { throw new RuntimeException(e); } if (value != null) { q.filter(field.getName(), value); } } return q; } }
Samraat said
I think you can also add a method of the form :
OKey getKeysByExample(T exampleObj){}
This is useful in scenarios where you have a million fan out problem. 🙂
David Chandler said
Sure, good idea.
mike lawrence said
does this look right?
public List<Key> getKeysByExample(T exampleObj) {
Query q = buildQueryByExample(exampleObj);
return asKeyList(q.fetchKeys());
}
David Chandler said
Objectify now provides list() and keyList() methods on Query, so asKeyList() and asList() in the DAO are no longer needed.
Haris Hashim said
Appreciate code for DAOBase?
TIA
David Chandler said
DAOBase is part of Objectify. See http://code.google.com/p/objectify-appengine/wiki/BestPractices
Regards, /dmc
Haris Hashim said
Hey thanks for the reply.
Somehow I miss the following code in your blog
import com.googlecode.objectify.helper.DAOBase;
Which I miss earlier. Thanks a lot!
Anh-Tuan said
Thank you! Could you please publish the configuration files and one entity specific DAO class.
Following your example, here are other methods I added in my GenericDAO. For performances and security issues, escapeFilter, escapeSortOrders, escapeProperties and escapeFetchLimit should be over-ridded in entity specific DAO class.
/*
* Convenience methods for updating multiple properties of an entity
* support embedded properties update (multi-level).
*
**/
public T updateProperties(T entity, Iterable properties) throws Exception{
List eps = escapeProperties(properties);
for(Property p : eps){
entity = updateProperty(entity, p);
}
return entity;
}
public T updateProperty(T entity, Property property) throws Exception{
String [] fieldNames = property.getPropertyName().split("\\.");
int len=fieldNames.length;
Object entityField = entity;
for(int i=0; i< len-1; i++){
entityField = getField(entityField, fieldNames[i]);
}
setField(entityField, fieldNames[len-1], property.getPropertyValue());
return entity;
}
public List escapeProperties(Iterable properties) {
/*
* TODO: Security and performances issues should be treated here.
* Entity classes should override this method
*/
ArrayList eps = new ArrayList();
for (Property p : properties) {
eps.add(p);
}
return eps;
}
public Object getField(Object obj, String fieldName) throws Exception{
String methName = "get"+fieldName.toUpperCase().charAt(0)+fieldName.substring(1);
Method meth = obj.getClass().getMethod(methName);
return meth.invoke(obj);
}
public void setField(Object obj, String fieldName, Object value) throws Exception{
String methName = "set"+fieldName.toUpperCase().charAt(0)+fieldName.substring(1);
Class parametersTypes[]= new Class[1];
parametersTypes[0]=value.getClass();
Method meth = obj.getClass().getMethod(methName, parametersTypes);
Object argList[] = new Object[1];
argList[0] = value;
meth.invoke(obj, argList);
}
/**
* Convenience method to build all possible get Queries
*
* @param filters
* :query filters
* @param sortOrders
* : query sort orders
* @param limit
* : number of maximum fetched results
* @return Query
*/
public Query buildQuery(Iterable filters,
Iterable sortOrders, int limit) {
Query q = ofy().query(clazz);
List escapedFilters = escapeFilters(filters);
List escapedSortOrders = escapeSortOrders(sortOrders);
int escapedLimit = escapeFetchLimit(limit);
for (Filter f : escapedFilters) {
q.filter(f.getFilterCondition(), f.getFilterValue());
}
for (String so : escapedSortOrders) {
q.order(so);
}
q.limit(escapedLimit);
return q;
}
public List escapeFilters(Iterable filters) {
/*
* TODO: Security and performances issues should be treated here Entity
* classes should override this method
*/
ArrayList efs = new ArrayList();
for (Filter f : filters) {
efs.add(f);
}
return efs;
}
public List escapeSortOrders(Iterable sortOrders) {
/*
* TODO: Security and performances issues should be treated here Entity
* classes should override this method
*/
ArrayList esos = new ArrayList();
for (String so : sortOrders) {
esos.add(so);
}
return esos;
}
public int escapeFetchLimit(int limit) {
/*
* TODO: Security and performances issues should be treated here Entity
* classes should override this method example:
*/
int classSpecificLimit = 51;
if (limit >= classSpecificLimit) {
return classSpecificLimit;
}
return limit;
}
/**
* Convenience method to get all objects for all possible get Queries
*
* @param filters
* :query filters
* @param sortOrders
* : query sort orders
* @param limit
* : number of maximum fetched objects
* @return List
*/
public List listByQuery(Iterable filters,
Iterable sortOrders, int limit) {
Query q = buildQuery(filters, sortOrders, limit);
return asList(q.fetch());
}
private List asList(Iterable iterable) {
ArrayList list = new ArrayList();
for (T t : iterable) {
list.add(t);
}
return list;
}
/**
* Convenience method to get all object keys for all possible get Queries
*
* @param filters
* :query filters
* @param sortOrders
* : query sort orders
* @param limit
* : number of maximum fetched keys
* @return List<Key>
*/
public List<Key> listKeysByQuery(Iterable filters,
Iterable sortOrders, int limit) {
Query q = buildQuery(filters, sortOrders, limit);
return asKeyList(q.fetchKeys());
}
private List<Key> asKeyList(Iterable<Key> iterableKeys) {
ArrayList<Key> keys = new ArrayList<Key>();
for (Key key : iterableKeys) {
keys.add(key);
}
return keys;
}
Here follows the code for Property end Filter
import java.io.Serializable;
public class Property implements Serializable{
private static final long serialVersionUID = -7165134355494456946L;
private String propertyName;
private Serializable propertyValue;
private Property(){}
public Property(String propertyName, Serializable propertyValue){
this.propertyName=propertyName;
this.propertyValue=propertyValue;
}
public void setPropertyName(String propertyName){
this.propertyName=propertyName;
}
public void setPropertyValue(Serializable propertyValue){
this.propertyValue=propertyValue;
}
public String getPropertyName(){
return propertyName;
}
public Serializable getPropertyValue(){
return propertyValue;
}
}
import java.io.Serializable;
public class Filter implements Serializable{
private static final long serialVersionUID = -7165134355494456946L;
private String filterCondition;
private Serializable filterValue;
private Filter(){}
public Filter(String filterCondition, Serializable filterValue){
this.filterCondition=filterCondition;
this.filterValue=filterValue;
}
public void setFilterCondition(String filterCondition){
this.filterCondition=filterCondition;
}
public void setFilterValue(Serializable filterValue){
this.filterValue=filterValue;
}
public String getFilterCondition(){
return filterCondition;
}
public Serializable getFilterValue(){
return filterValue;
}
}
“Generic” Objectified ActionHandler for gwt-dispatch « TurboManage said
[…] Generic DAO for Objectify 2 […]
Jeremy Wallez said
Hi,
Thanks for sharing your generic dao!
I think you can use an empty constructor and get the class using the parameterized type like this :
public class ObjectifyGenericDao extends DAOBase
{
protected Class clazz;
@SuppressWarnings("unchecked")
public ObjectifyGenericDao(){
clazz = ((Class) ((ParameterizedType) getClass()
.getGenericSuperclass()).getActualTypeArguments()[0]);
}
...
This is more efficient because you don’t need to write a Constructor for each DAO that extends ObjectifyGenericDao.
David Chandler said
Thanks for the empty constructor magic–works great. And sorry for the late acknowledgment–I’m way behind on correspondence.
Viswa said
from the original code,
I got this Compile error
Type mismatch: cannot convert from Map<Key,T> to List<Key>
public List<Key> putAll(Iterable entities)
{
return ofy().put(entities);
}
Viswa said
It should be
public Map<Key, T> putAll(Iterable entities)
{
return ofy().put(entities);
}
David Chandler said
Correct. The blog post was originally written for a prior version of the Objectify API.
Dennis said
Hi David and Anh-Tuan,
Thank you for sharing your code in this blog and comments.
I’ve used it quite successfully to make a generic class that helps making performing Objectify operations easier. However, sometimes when I want to perform something a little more tricky I have to slip back into the standard Objectify way of doing it.
Anyways, I’m planning later to write a blog entry to describe some of the things I’ve created using GAE and Objectify.
Again, thanks for sharing David and Anh-Tuan.
Keith said
Thanks for this, David!
I started with your GenericDao approach and took it a few steps further. The GenericDao is now the basis for a Manager layer that can be extended to override GenericDao methods as needed or add new biz logic-specific methods–and all without forcing any refactoring elsewhere in the code.
I wrote up a pretty extensive explanation of why this approach makes sense and included tons of code samples.
I’d love feedback, criticisms, or further suggestions.
http://blog.essaytagger.com/2011/07/elegant-coding-with-objectify-java.html
David Chandler said
Very nice article, Keith. Thanks for sharing.
Jacob said
Consider using this trick to obtain runtime class:
this.clazz = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
It can be little unsecure though, I usually provide two versions of constructor.
David Chandler said
Thanks, Jacob. I think an earlier commenter also recommended this, and I implemented it here: http://code.google.com/p/listwidget/source/browse/trunk/src/main/java/com/turbomanage/listwidget/server/service/ObjectifyDao.java
Andy DeWolfe said
queryByExample() using a @Parent property didn’t work for me. I made the following change to this method and now it works as expected ( or at least how i expect it to work )
private Query buildQueryByExample(T exampleObj)
{
Query q = ofy().query(clazz);
// Add all non-null properties to query filter
for (Field field : clazz.getDeclaredFields())
{
// Ignore transient, embedded, array, and collection properties
if (field.isAnnotationPresent(Transient.class)
|| (field.isAnnotationPresent(Embedded.class))
|| (field.getType().isArray())
|| (Collection.class.isAssignableFrom(field.getType()))
|| ((field.getModifiers() & BAD_MODIFIERS) != 0))
continue;
field.setAccessible(true);
Object value;
try
{
value = field.get(exampleObj);
}
catch (IllegalArgumentException e)
{
throw new RuntimeException(e);
}
catch (IllegalAccessException e)
{
throw new RuntimeException(e);
}
if (value != null)
{
// If @Parent field, set the query ancestor.
if( field.isAnnotationPresent(Parent.class) ) { // <—- ADDED THIS CHECK
q.ancestor(value);
} else {
q.filter(field.getName(), value);
}
}
}
return q;
}
Could/should probably do this same thing for all get/list methods as well.
David Chandler said
Thanks, Andy.
Urahara Kisuke said
Hello,
First thanks for this work.
I also have a problem with the getByExample query, when retrieving the result I have an NoSuchElementException.
Does anyone have any idea about where it should come from ?
java.util.NoSuchElementException
at com.google.appengine.api.datastore.QueryResultIteratorImpl.next(QueryResultIteratorImpl.java:74)
at com.google.appengine.api.datastore.QueryResultIteratorImpl.next(QueryResultIteratorImpl.java:27)
at com.googlecode.objectify.util.TranslatingIterator.next(TranslatingIterator.java:35)
at com.okiro.server.dao.ObjectifyDao.getByExample(ObjectifyDao.java:102)
Thanks,
David Chandler said
Hi, Urahara. The only way I could see this happening is if a result in the query was deleted while the query was in progress; otherwise, it might be an App Engine bug.
A Recipe for building RESTful Services « the bricklayers said
[…] Objectify, a simple-to-use framework based on the Low-Level API to access the DataStore. With the DAO implementation suggested by David Chandler, my data access layer was composed by only this […]
handling GWT RequestFactory server error responses | Ask Programming & Technology said
[…] I am using Chandler’s Generic DAO for Objectify 2 at https://turbomanage.wordpress.com/2010/02/09/generic-dao-for-objectify-2/ […]