views:

867

answers:

3

Does anyone have a good example for how to do a findByExample in JPA that will work within a generic DAO via reflection for any entity type? I know I can do it via my provider (Hibernate), but I don't want to break with neutrality...

Seems like the criteria API might be the way to go....but I am not sure how to handle the reflection part of it.

+1  A: 

Criteria API is your best bet. You'll need a JPA-2.0 provider for that, though. So if you have an entity like this:

@Entity
public class Foo {
    @Size(max = 20)
    private String name;
}

The following unit test should succeed (i tested it with EclipseLink, but it should work with any of the JPA-2.0 providers):

@PersistenceContext
private EntityManager em;

@Test
@Transactional
public void testFoo(){
    Foo foo = new Foo();
    foo.setName("one");
    em.persist(foo);
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Foo> c = cb.createQuery(Foo.class);
    Root<Foo> f = c.from(Foo.class);
    c.select(f).where(cb.equal(f.get("name"), "one"));
    TypedQuery<Foo> query = em.createQuery(c);
    Foo bar = query.getSingleResult();
    Assert.assertEquals("one", bar.getName());
}

Also, you might want to follow the link to the tutorial referenced here.

wallenborn
This is a good example, thanks. However -- and I will edit the question -- As I only have one DAO implementation (ala dont-repeat-the-dao), I am looking for a generic one that uses reflection to build the where clause regardless of the entity type. Any ideas?
HDave
+8  A: 

Actually, Query By Example (QBE) has been considered for inclusion in the JPA 2.0 specification but is not included, even if major vendors support it. Quoting Mike Keith:

I'm sorry to say that we didn't actually get to do QBE in JPA 2.0. Criteria API does not have any special operators for it so entity equality is just like in JP QL, based on PK value. Sorry, but hopefully we'll be more successful on that front in the next go-round. For now it is one of those vendor features that every vendor supports, but is not in the spec yet.

Just in case, I've added (non generic) sample code for the major vendors below for documentation purposes.

EclipseLink

Here is a sample of using QBE in the EclipseLink JPA 2.0 reference implementation:

// Create a native EclipseLink query using QBE policy
QueryByExamplePolicy policy = new QueryByExamplePolicy();
policy.excludeDefaultPrimitiveValues();
ReadObjectQuery q = new ReadObjectQuery(sampleEmployee, policy);

// Wrap the native query in a standard JPA Query and execute it 
Query query = JpaHelper.createQuery(q, em); 
return query.getSingleResult(); 

OpenJPA

OpenJPA supports this style of query through its extended OpenJPAQueryBuilder interface:

CriteriaQuery<Employee> q = cb.createQuery(Employee.class);

Employee example = new Employee();
example.setSalary(10000);
example.setRating(1);

q.where(cb.qbe(q.from(Employee.class), example);

Hibernate

And with Hibernate's Criteria API:

// get the native hibernate session
Session session = (Session) getEntityManager().getDelegate();
// create an example from our customer, exclude all zero valued numeric properties 
Example customerExample = Example.create(customer).excludeZeroes();
// create criteria based on the customer example
Criteria criteria = session.createCriteria(Customer.class).add(customerExample);
// perform the query
criteria.list();

Now, while it should be possible to implement something approaching in a vendor neutral way with JPA 2.0 Criteria API and reflection, I really wonder if it's worth the effort. I mean, if you make any of the above snippets generic and put the code in a DAO method, it would be quite easy to switch from one vendor to another if the need should arise. I agree it's not ideal, but still.

References

Pascal Thivent
If I am reading your code correctly, this implements findAll, not findByExample. There is no where clause that I can detect. I need to use some kind of reflect to determine the ORM mapped properties of the entity that have a scalar value.
HDave
You're right and I have revamped my answer. Need to think more about this.
Pascal Thivent
I am confused by Mike Keith's response. The criteria API in JPA 2 is more than capable of handling a where clause needed for QBE. It's a bunch of equality tests concatenated by "AND".
HDave
@HDave He's not saying the Criteria API is not capable, he's saying the Criteria API doesn't provide any method for QBE out of the box.
Pascal Thivent
@Pascal: Insightful answer! I hadn't realized that the three frameworks have so different ideas of what an example actually is.
wallenborn
@wallenborn: It's pretty sad the expert group didn't standardized QBE in JPA 2.0, especially since the major implementations support it. I really wonder why they couldn't make it.
Pascal Thivent
Yes, they could have picked any of the three and it would have been fine. Why they didn't do that? Maybe there's an argument about whether the example object should be the actual object, or whether it should be sort of an extension of the Criteria query. The former allows the example to be created very far away from the JPA layer, with no additional overhead. The latter can be more powerful, but you risk having to ask the persistence layer to create it for you. I dunno which is the better approach...
wallenborn
+4  A: 

This is quite crude and i'm not convinced it's a good idea in the first place. But anyway, let's try to implement QBE with the JPA-2.0 criteria API.

Start with defining an interface Persistable:

public interface Persistable {
    public <T extends Persistable> Class<T> getPersistableClass();
}

The getPersistableClass() method is in there because the DAO will need the class, and i couldn't find a better way to say T.getClass() later on. Your model classes will implement Persistable:

public class Foo implements Persistable {
    private String name;
    private Integer payload;

    @SuppressWarnings("unchecked")
    @Override
    public <T extends Persistable> Class<T> getPersistableClass() {
        return (Class<T>) getClass();
    }
}

Then your DAO can have a findByExample(Persistable example) method (EDITED):

public class CustomDao {
    @PersistenceContext
    private EntityManager em;

    public <T extends Persistable> List<T> findByExample(T example) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, SecurityException, NoSuchMethodException {
        Class<T> clazz = example.getPersistableClass();
        CriteriaBuilder cb = em.getCriteriaBuilder();
        CriteriaQuery<T> cq = cb.createQuery(clazz);
        Root<T> r = cq.from(clazz);
        Predicate p = cb.conjunction();
        Metamodel mm = em.getMetamodel();
        EntityType<T> et = mm.entity(clazz);
        Set<Attribute<? super T, ?>> attrs = et.getAttributes();
        for (Attribute<? super T, ?> a: attrs) {
            String name = a.getName();
            String javaName = a.getJavaMember().getName();
            String getter = "get" + javaName.substring(0,1).toUpperCase() + javaName.substring(1);
            Method m = cl.getMethod(getter, (Class<?>[]) null);
            if (m.invoke(example, (Object[]) null) !=  null)
                p = cb.and(p, cb.equal(r.get(name), m.invoke(example, (Object[]) null)));
        }
        cq.select(r).where(p);
        TypedQuery<T> query = em.createQuery(cq);
        return query.getResultList();
    }

This is quite ugly. It assumes getter methods can be derived from field names (this is probably safe, as example should be a Java Bean), does string manipulation in the loop, and might throw a bunch of exceptions. Most of the clunkiness in this method revolves around the fact that we're reinventing the wheel. Maybe there's a better way to reinvent the wheel, but maybe that's where we should concede defeat and resort to one of the methods listed by Pascal above. For Hibernate, this would simplify the Interface to:

public interface Persistable {}

and the DAO method loses almost all of its weight and clunkiness:

@SuppressWarnings("unchecked")
public <T extends Persistable> List<T> findByExample(T example) {       
    Session session = (Session) em.getDelegate();
    Example ex = Example.create(example);
    Criteria c = session.createCriteria(example.getClass()).add(ex);
    return c.list();
}

EDIT: Then the following test should succeed:

@Test
@Transactional
public void testFindFoo() {
    em.persist(new Foo("one",1));
    em.persist(new Foo("two",2));

    Foo foo = new Foo();
    foo.setName("one");
    List<Foo> l = dao.findByExample(foo);
    Assert.assertNotNull(l);
    Assert.assertEquals(1, l.size());
    Foo bar = l.get(0);
    Assert.assertNotNull(bar);
    Assert.assertEquals(Integer.valueOf(1), bar.getPayload());      
}
wallenborn
I think you are on the right track here. Instead of snagging methods that start with "get"...can we snag any private field that had a JPA @Column annotation? Actually -- it just occured to me that we might look at the implementation of the Hibernate queryByExample for clues....
HDave
Hibernate uses the metamodel, which is definitely better than using reflection. I updated the finder method above accordingly. We still have to do String manipulation, but this time in a safer way: the metamodel tells us the name of the Java member, and the assumption that the getter for field is getField() is safe for Java Beans. The whole thing is still quite complicated, though, and declares lots of exceptions.
wallenborn
Interesting, but what if I use a `boolean` attribute and a `isXxx()` accessor? what about `@Transient` (might not be an issue though)? What about all these horrible exceptions :) I'm not saying it's not doable (and thank you for the code), I'm just wondering if its worth the effort and the troubles.
Pascal Thivent
Yeah, it's probably not worth it. Using the QBE of the underlying JPA provider is the smarter decision. If you want to change the provider, you only have to change the implementation of the DAO's findByExample, or, if you are picky about that, implement the method for all providers in a separate unit each (Strategy class for example, or you could make GenericDAO abstract and have a HibernateDAO implement it) and have the particular implementation injected at runtime.
wallenborn
+1 anyway. And thanks for the work, it can still give some ideas (dammit, why didn't they include QBE... this is frustrating).
Pascal Thivent