i'm currently writing a generic data access object for the persistent entities I have to handle in my JavaEE application. What this codeReview is about is a findByExample method that is used to find database entries on the basis of a given example object. This may already be implemented for openJPA but I'm bound to openJPA snapshot 1.2.x (old) and I couldn't find it anywhere in the manual. please tell me about the pitfalls, improvements and bad practices you see in my code. I'm willing to learn.
Here we go
Using it as following
MyEntity me = new MyEntity();
me.setName("john")
me.setAge(18);
entityAccess.findByExample(me);
will create the following JPQL query using only the parameters that were set
"SELECT x FROM MyEntity x WHERE x.name = :name AND x.age = :age"
Then it will set the parameters and exequte the query.
This is how I implemented it.
The two public methods (BasicEntity is a mappedSuperClass)
public <T extends BasicEntity> List<T> findAllByExample(T entity) {
return getQueryFromExample(entity).getResultList();
}
public <T extends BasicEntity> T findByExample(T entity) throws NoResultException, NonUniqueResultException {
return (T) getQueryFromExample(entity).getSingleResult();
}
The method thats creates the Query
and sets the parameters. (em is the jpa entityManager)
private <T extends BasicEntity> Query getQueryFromExample(T entity) {
HashMap<String, Object> fieldNameValuePairs = getFieldNameValuePairs(entity.getClass(), entity);
/* build query string */
StringBuilder queryString = new StringBuilder();
queryString.append("SELECT x FROM ");
queryString.append(entity.getClass().getSimpleName());
queryString.append(" x WHERE ");
for (Iterator<String> iterator = fieldNameValuePairs.keySet().iterator(); iterator.hasNext();) {
String fieldName = iterator.next();
queryString.append("x." + fieldName + " = :" + fieldName);
if (iterator.hasNext()) {
queryString.append(" AND ");
}
}
/* create query and set parameters */
Query q = em.createQuery(queryString.toString());
for (Entry<String, Object> entry : fieldNameValuePairs.entrySet()) {
String fieldName = entry.getKey();
Object fieldValue = entry.getValue();
q.setParameter(fieldName, fieldValue);
}
return q;
}
The following method puts relevant and non null fields into a HashMap. It walks up the type hierarchy with a recursion. Object.getClass().getSuperClass
returns null
.
private HashMap<String, Object> getFieldNameValuePairs(Class<?> type, Object instance) {
if (type != null) {
HashMap<String, Object> fieldNameValuePairs = new HashMap<String, Object>();
for (Field field : type.getDeclaredFields()) {
/* we only care for persitent fields */
if (isPersistentField(field)) {
field.setAccessible(true);
String fieldName = field.getName();
Object fieldValue = null;
try {
fieldValue = field.get(instance);
} catch (Exception e) {
throw new RuntimeException(
"Error reflecting fields while building queryString for findByExample. Cannot read "
+ instance.getClass().getSimpleName() + "." + fieldName, e);
}
/* ignore unset fields */
if (!(fieldValue == null || fieldValue.equals(""))) {
fieldNameValuePairs.put(fieldName, fieldValue);
System.out.println(fieldName + " = " + fieldValue.toString());
}
}
}
/* recursive step up the type hierarchy */
fieldNameValuePairs.putAll(getFieldNameValuePairs(type.getSuperclass(), instance));
return fieldNameValuePairs;
} else {
return new HashMap<String, Object>();
}
}
the following section of code determines whether a field is relevant (since i don't want for example serialVersionUID
in my JPQL query)
I chose to use the fields annotations to decide, because I thought this makes the persisted fields unique.
private static final List<Class<? extends Annotation>> persistenceAnnotations = new ArrayList<Class<? extends Annotation>>();
static {
persistenceAnnotations.add(Id.class);
persistenceAnnotations.add(Column.class);
persistenceAnnotations.add(JoinColumn.class);
persistenceAnnotations.add(OneToOne.class);
persistenceAnnotations.add(ManyToOne.class);
persistenceAnnotations.add(OneToMany.class);
persistenceAnnotations.add(ManyToMany.class);
persistenceAnnotations.add(JoinColumns.class);
persistenceAnnotations.add(Basic.class);
persistenceAnnotations.add(Lob.class);
}
private boolean isPersistentField(Field field) {
for (Annotation annotation : field.getAnnotations()) {
if (persistenceAnnotations.contains(annotation.annotationType())) {
return true;
}
}
return false;
}
Aug29 - REFACTORED VERSION
private <T extends BasicEntity> Query getQueryFromExample(T entity) {
HashMap<String, Object> fieldNameValuePairs = getFieldNameValuePairs(entity.getClass(), entity);
/* build query string */
String queryString = String.format("SELECT x FROM %s WHERE %s", entity.getClass().getSimpleName(),
join(asJpqlConditions(fieldNameValuePairs.keySet(), "x"), " AND "));
/* create query and set parameters */
Query q = em.createQuery(queryString.toString());
for (Entry<String, Object> entry : fieldNameValuePairs.entrySet()) {
q.setParameter(entry.getKey(), entry.getValue());
}
return q;
}
private List<String> asJpqlConditions(Collection<String> fieldNames, String identifier) {
List<String> conditions = new ArrayList<String>();
for (String fieldName : fieldNames) {
conditions.add(String.format("%1$.%2$s = :%2$s", identifier, fieldName));
}
return conditions;
}
private String join(List<String> parts, String glue) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String part : parts) {
if (!first) {
sb.append(glue);
}
sb.append(part);
first = false;
}
return sb.toString();
}
private HashMap<String, Object> getFieldNameValuePairs(Class<?> type, Object instance) {
/* return empty HashMap in last recursion step (type is null) */
if (type == null) {
return new HashMap<String, Object>();
}
/* recursive call to fill the HashMap with the fields from supertypes */
HashMap<String, Object> fieldNameValuePairs = getFieldNameValuePairs(type.getSuperclass(), instance);
for (Field field : type.getDeclaredFields()) {
/* we only care for persitent fields */
if (!isPersistentField(field)) {
continue;
}
boolean wasAccessible = field.isAccessible();
field.setAccessible(true);
String fieldName = field.getName();
Object fieldValue = null;
try {
fieldValue = field.get(instance);
} catch (Exception e) {
throw new RuntimeException("Error reflecting fields while building queryString for findByExample. Cannot read "
+ instance.getClass().getSimpleName() + "." + fieldName, e);
}
field.setAccessible(wasAccessible);
/* ignore unset fields */
if (!(fieldValue == null || fieldValue.equals(""))) {
fieldNameValuePairs.put(fieldName, fieldValue);
}
}
return fieldNameValuePairs;
}
private boolean isPersistentField(Field field) {
for (Annotation annotation : field.getAnnotations()) {
if (annotation.annotationType().equals(PersistentField.class)) { //added custom marker annotation
return true;
}
}
return false;
}