Compile-time checking JPA queries
JPA provides several alternatives for querying data. Such alternatives may be classified attending to a variety of criteria, eg, language used (SQL vs JPQL) or whether queries are static (compilation time) or dynamic (execution time).
Static queries are defined using annotations @NamedQuery (javax.persistence.NamedQuery) and @NamedQueries (javax.persistence.NamedQueries) in the @Entity class definition itself:
@NamedQuery( name="findAllCustomersWithName", query="SELECT c FROM Customer c WHERE c.name LIKE :custName" )
On the other hand, EntityManager provides methods createQuery(…) y createNativeQuery(…) which take either a JPQL or a SQL query, respectively.
Thus, queries can be defined both in compilation or execution time.
(Note: It is advisable to always use parametrized queries using methods setParameter(…) from Query to avoid SQL Injection vulnerabilities.)
Criteria API
However, JPA provides an alternative approach to query objects: Criteria API. Indeed, one of the motivations to switch to JPA is to deal with objects rather than SQL dialects, isn’t it ?
Let’s look a sample code.
Entity definition:
@Entity public class User { @Id private Integer userId; @Basic @Column(length=15, nullable=false) private String name; @Basic @Column(length=64, nullable=false) private String userDigestedPasswd; @Basic @Column(length=50, nullable=true) private String email; @Basic @Column(nullable=false) public Integer privilegeLevel; @Basic @Column(nullable=false) private Boolean active; }
Let’s query db and check results (using JUnit):
public class UserTest { @Test public void testUserCriteria(){ EntityManagerFactory emf = null; EntityManager em = null; try { emf = Persistence.createEntityManagerFactory("criteria"); em = emf.createEntityManager(); final CriteriaBuilder cb = em.getCriteriaBuilder(); final CriteriaQuery<User> q = cb.createQuery(User.class); final Root<User> users = q.from(User.class); final Predicate condition = cb.equal(users.get("privilegeLevel"), 5); q.select(users).where(condition).orderBy(cb.asc(users.get("userId"))); em.getTransaction().begin(); List<User> result = em.createQuery(q).getResultList(); em.getTransaction().commit(); assertNotNull(result); assertEquals(2, result.size()); assertEquals(1, (int)result.get(0).getUserId()); assertEquals("Pepe", result.get(0).getName()); assertEquals(3, (int)result.get(1).getUserId()); assertEquals("Dolores", result.get(1).getName());} catch (Exception e) { fail("Unexpected Exception " + e.getMessage()); } finally { if (em != null) em.close(); if (emf != null) emf.close(); } } }
Following lines show query creation:
final CriteriaBuilder cb = em.getCriteriaBuilder(); final CriteriaQuery<User> q = cb.createQuery(User.class); final Root<User> users = q.from(User.class); final Predicate condition = cb.equal(users.get("privilegeLevel); q.select(users).where(condition).orderBy(cb.asc(users.get("userId
First of all, get a CriteriaBuilder from an EntityManager. Then, get a CriteriaQuery instance, setting the class to hold results. In our case, User.class:
final CriteriaBuilder cb = em.getCriteriaBuilder(); final CriteriaQuery<User> q = cb.createQuery(User.class);
Following, the Entity to run the query against must be set:
final Root<User> users = q.from(User.class);
Now it’s time to set query matching conditions. In the sample code, the condition is just attribute privilegeLevel to be equals to 5:
final Predicate condition = cb.equal(users.get("privilegeLevel"), 5);
Finally, query is built adding conditions on Root. Grouping and sorting options may be set too (ie, ascending sorting is set on userId):
q.select(users).where(condition).orderBy(cb.asc(users.get(“userId”)));
Please have a look at CriteriaBuilder for different options. Grouping and sorting options may be found at CriteriaQuery.
Using metamodel for compile-time checking
Note the query we have just build requires to keep track of object attributes names. Eg, to build the query, the name of the attribute privilegeLevel is used. However, if attribute name were changed later, the code would compile and only fail at runtime:
final CriteriaQuery<User> q = cb.createQuery(User.class); final Root<User> users = q.from(User.class); final Predicate condition = cb.equal(users.get("privilegeLevel"), 5); q.select(users).where(condition).orderBy(cb.asc(users.get("userId")));
That is no good.
Fortunately, using metamodel, we will be able to build compile-time checked queries. A brief introduction can be found at The Java EE6 Tutorial.
Using metamodel, the code will reference an SingularAttribute of the object rather than using a String holding the object attribute name. So, if object attribute were changed later, the compiler would flag it for us.
First of all, the correspondent metamodel class (EntityType) must be created. Although it can achieved by several ways, probably the easiest one, for openJPA implementation, is to add a openJPA build flag: -Aopenjpa.metamodel=true.
So we have the class User_ created, which is the correspondent metamodel class for User:
* Generated by OpenJPA MetaModel Generator Tool. **/ package com.wordpress.tododev.criteria.entities; import javax.persistence.metamodel.SingularAttribute; @javax.persistence.metamodel.StaticMetamodel (value=com.wordpress.tododev.criteria.entities.User.class) @javax.annotation.Generated (value="org.apache.openjpa.persistence.meta.AnnotationProcessor6",date="Mon Mar 04 16:47:46 CET 2013") public class User_ { public static volatile SingularAttribute<User,Boolean> active; public static volatile SingularAttribute<User,String> email; public static volatile SingularAttribute<User,String> name; public static volatile SingularAttribute<User,Integer> privilegeLevel; public static volatile SingularAttribute<User,String> userDigestedPasswd; public static volatile SingularAttribute<User,Integer> userId; }
If such class were added to code repo, any later change to class User would remain unnoticeable. Moreover, it is not a good idea to add auto-generated items to code versioning systems.
Using ant, maven or similar tools, a target could be added to create metamodel classes. Such target should be executed after any change to JPA Entities.
Also possible to use IDE for that. Eg, for those using Eclipse, just need to add the already mentioned compilation flag to Properties->Java Compiler->Annotation Processor and the lib (jar) containing the Annotation Processor for the chosen JPA implementation to section Factory Path within Annotations Processor (could lead to compilation issues in auto mode, provided that metamodel class must be compiled before the code using it).
Let us add another test to the suite. This one will not provide a String containing the attribute name, but use the metamodel class instead:
@Test public void testUserCriteriaMetaModel(){ EntityManagerFactory emf = null; EntityManager em = null; try { emf = Persistence.createEntityManagerFactory("criteria"); em = emf.createEntityManager(); final CriteriaBuilder cb = em.getCriteriaBuilder(); final CriteriaQuery<User> q = cb.createQuery(User.class); final Metamodel m = em.getMetamodel(); final Root<User> user = q.from(m.entity(User.class)); final Predicate condition = cb.equal(user.get(User_.privilegeLevel), 5); q.select(user).where(condition).orderBy(cb.asc(user.get(User_.userId))); em.getTransaction().begin(); List<User> result = em.createQuery(q).getResultList(); em.getTransaction().commit(); assertNotNull(result); assertEquals(2, result.size()); assertEquals(1, (int)result.get(0).getUserId()); assertEquals("Pepe", result.get(0).getName()); assertEquals(3, (int)result.get(1).getUserId()); assertEquals("Dolores", result.get(1).getName()); } catch (Exception e) { fail("Unexpected Exception " + e.getMessage()); } finally { if (em != null) em.close(); if (emf != null) emf.close(); } }
More relevant changes are user.get(User_.privilegeLevel) instead of users.get(“privilegeLevel”) and user.get(User_.userId) instead of users.get(“userId”).
- Download source code from GitHub.
Reference: | Compile-time checking JPA queries from our JCG partner Sergio Molina at the TODOdev blog. |
Hi,
Good article. Maybe you can cover QueryDSL framework also, which is another level above Criteria and much easier to use
Thanks Pravin, will have a look ;-)