Menu

Official website

Syntactically correct and type-safe JPA queries in Play 2.0


16 Apr 2012

min read

When writing a Play 2.0 Java web application it is likely you’re not only persisting data, you also want to retrieve your persisted data. One of the options is using the Java Persistence API (JPA), version 2. When using JPA, there are several ways to read data from your database. In this article we’ll explain three different approaches, and we’ll discuss the syntax correctness and type-safety of the approaches.

For all three approaches we’ll use the same use case: find a User object by its `username' property.

Java Persistence API query language

An easy way to read data from our database is by creating a dynamic query' with use of the Java Persistance API (JPA) query language, a string-based query language. To find a `User by username we would write something like:

try {
    return JPA.em()
        .createQuery("from User where username = :username", User.class)
        .setParameter("username", "foo").getSingleResult();
} catch (NoResultException nre) {
    return null;
} catch (NonUniqueResultException nure) {
    return null;
}

The problem with this example is syntax checking. The query language is not checked at compile time, so if we write from Uzer instead of from User no compile-time errors will occur. However, because the query is incorrect, we will get a runtime error. To prevent these kind of runtime errors we need a way to create syntactically checked queries. This is where the JPA Criteria API comes in.

JPA Criteria API

The Criteria API is an alternative to the query language. It allows us to define object-based queries instead of the string-based approach of the query language. The advantage is that these object-based queries are more syntactically checked.

If we want to find the User with help of the Criteria API it becomes this piece of code:

try {
    CriteriaBuilder cb = JPA.em().getCriteriaBuilder();
    CriteriaQuery<User> query = cb.createQuery(User.class);
    Root<User> user = query.from(User.class);
    query.where(cb.equal(user.get("username"), "foo"));
    return JPA.em().createQuery(query).getSingleResult();
} catch (NoResultException nre) {
    return null;
} catch (NonUniqueResultException nure) {
    return null;
}

Here the compiler will give us an error if we write Uzer.class instead of User.class. So more syntax checking. But as you probably already noticed, user.get("username") is still not checked for correct syntax. Our code will successfully compile if we replace username with uzrname. This is because the compiler doesn’t know if uzrname is a property of the User class. So we need more syntax checking to prevent these kind of errors. With the help of Java Persistence Metamodel API we can create fully syntactically checked queries.

JPA 2.0 Metamodels

A metamodel class is a class that represents the datastructure of the corresponding model class. For example, this model class:

@Entity
public class User {
    public Long id;
    public String username;
}

Would have the following metamodel class:

@StaticMetamodel(User.class)
public abstract class User_ {
    public static volatile SingularAttribute&lt;User, String&gt; username;
}

Now we are able to create a type-safe query:

try {
    CriteriaBuilder cb = JPA.em().getCriteriaBuilder();
    CriteriaQuery<User> query = cb.createQuery(User.class);
    Root<User> user = query.from(User.class);
    query.where(cb.equal(user.get(User_.username), "foo"));
    return JPA.em().createQuery(query).getSingleResult();
} catch (NoResultException nre) {
    return null;
} catch (NonUniqueResultException nure) {
    return null;
}

These metamodels are pretty useful because our queries are syntactically correct and type-safe! Syntactically correct because all the syntax is checked. We will get an error is we replace username by uzername. So if we, for example, rename a variable, the compiler will tell us which queries we forgot to update. We won’t get a runtime error.

The query is also type-safe. Type safety means that the compiler will validate types while compiling. If a wrong type is assigned to a variable an exception is thrown at compile time. As you probably know Play 2.0 is focused on type safety.

Because of the metamodels, the compiler knows the type of the variables. So you can’t for example do:

try {
    CriteriaBuilder cb = JPA.em().getCriteriaBuilder();
    CriteriaQuery<User> query = cb.createQuery(User.class);
    Root<User> user = query.from(User.class);
    query.like(cb.like(user.get(User_.id), "1"));
    return JPA.em().createQuery(query).getSingleResult();
} catch (NoResultException nre) {
    return null;
} catch (NonUniqueResultException nure) {
    return null;
}

This query won’t compile because the like method needs a Expression<String> as first parameter and id is a Expression<Long>.

Because it takes some time to write these metamodels by hand, it would be nicer to generate them. The next paragraph will explain how to generate metamodels in Play 2.0.

JPA 2.0 Metamodel genaration in Play 2.0

A way to generate our metamodels is by using the org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor. The processor will automatically run if the hibernate-jpamodelgen.jar is added to the classpath and when you are using JDK 6. So we add a project dependecy to our Build.scala:

val appDependencies = Seq(
    "org.hibernate" % "hibernate-jpamodelgen" % "1.2.0.Final"
)

And specify a folder where the generated source files are placed. This is done by passing an argument to the Java compiler. Passing an argument is also done in Build.scala:

val main = PlayProject(appName, appVersion, appDependencies, mainLang = JAVA).settings(
    javacOptions ++= Seq("-s", "metamodel")
)

Note that the provided folder must exist, the Java compiler won’t generate it for you. So it is probably not a good idea to put the metamodels in our target folder, because its contents are deleted when the the play clean command is run.

Eclipse users can add the metamodel folder to their `source folders' (Project → Properties → Java Build Path → Source → Add Folder) for autocompletion etc.

Conclusion

Using the Criteria API with metamodels gives us the opportunity to write syntactically correct and type-safe queries. It is also quite easy to generate these metamodels. So with a bit of effort we can get nice object-based, syntactically correct and type-safe queries that cause fewer runtime errors.

expand_less