Hi all
We have recently implemented some query by example functionality for hibernate. This post is aimed at seeing if there is already existing hibernate functionality which provides this feature.
now i realise that the following API exists
Code:
Example exampleUser = Example.create(u)
.ignoreCase() .enableLike(MatchMode.ANYWHERE) .excludeProperty("password");
return getSession().createCriteria(User.class) .add(exampleUser)
.list();
However the drawbacks of this method is that
1. We have to create a "special" empty version of out business objects since new User() would initialise some fields to not null values.
2. How can can i distinguish between null = "i dont care whats this value is" and null = "this value must be not set"
3. How do i create disjuctions where a property could be equal to several possible values.
4. Generally i just be more comfortable with being explicit about whats criteria im searching on rather than using a object state
So why not use explicit Critera you ask?
Code:
List rules = ((HibernateEntityManager)getEntityManager()).getSession().createCriteria(User.class)
.add( Restrictions.eq("username", username) )
.list();
I think having the "username" field as a hardcoded string is a problem here. Its another thing I need to get right and another thing that could go wrong if i refactor the property (to be called "userName" for example)
So..... Heres what we came up with
Code:
Criteria add = ((HibernateEntityManager)getEntityManager()).getSession().createCriteria(User.class);
User user = query(User.class);
user.setUsername("username");
return (((QueryByExample)user).populateCriteria(add)).list();
Basically we used some dynamic proxy hackery in the "query()" method to return a proxyed User object. This proxy records what "setProperty()" method invocations are being called as well as the arguments used. The proxy also dynamically impliments the QueryByExample interface which has the populate critera method which takes a criteria object and populates it with the all the criteria resulting from the methods invoked on the proxy.
so
1. The business object does not need to change in anyway to accomodate query by example. It doesnt need to extends or implement anything for that purpose
2. Im being explicit about what properties im searching on by invoking the specific setter properties. I dont have to guess/specify what criteria is important from looking a object state
3. I can easily query based on disjuctions by simple calling the same setter multiple times with different values
The proxy hackery looks like this. We have used ClassImposterizer from mockito
Code:
public static interface QueryByExample {
Criteria populateCriteria(Criteria criteria);
}
protected static <T> T query(Class<T> clazz) {
final T impostor = ClassImposterizer.INSTANCE.imposterise(new MethodInterceptor() {
private QueryBuilder queryBuilder = new QueryBuilder();
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
if (method.getName().equals("populateCriteria")) {
return queryBuilder.populateCriteria((Criteria) args[0]);
} else if (method.getName().startsWith("set")) {
String fieldName = method.getName().substring(3, 4).toLowerCase()+method.getName().substring(4);
queryBuilder.addField(fieldName, args[0]);
}
return null;
}
}, clazz, QueryByExample.class);
return impostor;
}
static class QueryBuilder {
private Map<String, List<Object>> values = new HashMap<String, List<Object>>();
public void addField(String name, Object value) {
if(values.get(name)==null)
values.put(name, new ArrayList<Object>());
values.get(name).add(value);
}
public Criteria populateCriteria(Criteria criteria) {
for (Map.Entry<String, List<Object>> entry : values.entrySet()) {
if(entry.getValue().size()==1)
criteria.add(Restrictions.eq(entry.getKey(), entry.getValue().get(0)));
else
criteria.add(getDisjunction(entry.getKey(), entry.getValue()));
}
return criteria;
}
private Criterion getDisjunction(String key, List<Object> values) {
Disjunction disjunction = Restrictions.disjunction();
for(Object obj : values) {
disjunction.add(Restrictions.eq(key, obj));
}
return disjunction;
}
}