Red Hat

A typesafe criteria query API for JPA

Posted by Gavin King    |    Dec 16, 2008    |    Tagged as Hibernate ORM JPA

The public draft of the JPA 2.0 specification is already out and includes a much-awaited feature: an API that lets you create queries by calling methods of Java objects, instead of by embedding JPA-QL into strings that are parsed by the JPA implementation. You can learn more about the API proposed by the public draft at Linda's blog.

There's several reasons to prefer the API-based approach:

  • It's easier to build queries dynamically, to handle cases where the query structure varies depending upon runtime conditions.
  • Since the query is parsed by the Java compiler, no special tooling is needed in order to get syntactic validation, autocompletion and refactoring support.

(Note that JPA-QL syntax validation and autocompletion is available is some IDEs - in JBoss Tools, for example.)

There's two major problems with criteria query APIs in the Java language:

  • The queries are more verbose and less readable.
  • Attributes must be specified using string-based names.

The first problem isn't really solvable without major new language features (usually described as DSL support). The second problem could easily be solved by adding a typesafe literal syntax for methods and fields to Java. This is now a sorely needed feature of the language, it's especially useful in combination with annotations.

There have been some previous efforts to work around the lack of method and field literals. One recent example is LIQUidFORM. Unfortunately that particular approach forces you to represent every persistent attribute as a public getter method, which is not a restriction that is acceptable in the JPA specification.

I've proposed a different approach to the JPA EG. This approach comes in three layers:

  • A metamodel API for JPA
  • A query API where types and attributes are specified in terms of metamodel API objects
  • Support for third-party tooling which would generate a typesafe metamodel from the entity classes

Let's go layer-by layer.

The Metamodel

The metamodel API is a bit like the Java reflection API, except that it is provided by the JPA persistence provider, is aware of the JPA metadata, and uses generics in a clever way. (Also it uses unchecked exceptions.)

For example, to obtain an object that represents an entity, we call the MetaModel object:

import javax.jpa.metamodel.Entity;
...
Entity<Order> order = metaModel.entity(Order.class);
Entity<Item> item = metaModel.entity(Item.class);
Entity<Product> item = metaModel.entity(Product.class);

To obtain attributes of the entity, we need to use string-based names, as usual:

import javax.jpa.metamodel.Attribute;
import javax.jpa.metamodel.Set;
...
Set<Order, Item> orderItems = order.set("items", Item.class);
Attribute<Item, Integer> itemQuantity = item.att("quantity", Integer.class);
Attribute<Item, Product> itemProduct = item.att("product", Product.class);
Attribute<Product, BigDecimal> productPrice = product.att("price", BigDecimal.class)

Notice how the metamodel types which represent attributes are parameterized not only by the type of the attribute they represent, but also by the type that they belong to.

Also notice that this code is non-typesafe and can fail at runtime if no persistent attribute with the given type and name exists in the entity class. This is the only non-typesafe code we'll see - our goal is keep the rest of the API completely typesafe. How does that help us? Well, the trick here is to notice that the metamodel objects represent completely static information about a persistent classes, state that doesn't change at runtime. So we can:

  • obtain and cache these objects at system intialization time, forcing any errors to occur upfront, or even
  • let a tool that has access to our persistent classes generate the code that obtains and caches metamodel objects.

That's much better than having these errors occur at query execution time, as they do in the previous criteria query proposal.

The metamodel API is generally useful, even independent of the query API. Currently it's very difficult to write generic code that interacts with JPA because JPA metadata may be partitioned between annotations and various XML documents.

But, of course, the most popular use of the metamodel is to build queries.

Queries

To construct a query, we pass metamodel objects to the QueryBuilder API:

Query query = queryBuilder.create();

Root<Order> orderRoot = query.addRoot(order);
Join<Order, Item> orderItemJoin = orderRoot.join(orderItems);
Join<Item, Product> itemProductJoin = orderItemJoin.join(itemProduct);

Expression<Integer> quantity = orderItemJoin.get(itemQuantity);
Expression<BigDecimal> price = itemProductJoin.get(productPrice);

Expression<Number> itemTotal = queryBuilder.prod(quantity, price);
Expression<Boolean> largeItem = queryBuilder.gt(itemTotal, 100);

query.restrict(largeItem)
     .select(order)
     .distinct(true);

For comparison, here is the same query expressed using the API proposed in the public draft:

Query query = queryBuilder.createQueryDefinition();

DomainObject orderRoot = query.addRoot(Order.class);
DomainObject orderItemJoin = orderRoot.join("items");
DomainObject itemProductJoin = orderItemJoin.join("product");

Expression quantity = orderItemJoin.get("quantity");
Expression price = itemProductJoin.get("price");

Expression itemTotal = quantity.times(price);
Predicate largeItem = queryBuilder.greaterThan(100);

query.where(largeItem);
     .selectDistinct(order);

Of course, this query could be written more compactly in either API, but I'm trying to draw attention to the generic types of the objects that make up the query. The type parameters prevent me from writing something like this:

orderItemJoin.get(productPrice); //compiler error

The use of generics means the compiler can detect when we try to create a path expression by combining a queried entity of one type and an attribute of some other type. The metamodel object productPrice has type Attribute<Product, BigDecimal> and therefore cannot be passed to the get() method of orderItemJoin. get() only accepts Attribute<Item, ?>, since orderItemJoin is of type Join<Order, Item>.

Expressions are also parameterized by the expression type, so the compiler detect mistakes like:

queryBuilder.gt(stringExpression, numericExpression); //error

Indeed, the API has sufficient typesafeness that it's more or less impossible to build an unexecutable query.

Generating a typesafe metamodel

It's completely possible to build queries with only the metamodel API and the query API. But to really make the most of these APIs, the final piece of the puzzle is a little code generation tool. This tooling doesn't need to be defined by the JPA specification, and different tools don't need to generate exactly the same code. Nevertheless, the generated code will always be portable between all JPA implementations. All the tool does is reflect upon the persistent entities and create a class or classes that statically cache references to the metamodel Entity and Attribute objects.

Why do we need this code generator? Because writing Attribute<Item, Integer> itemQuantity = item.att("quantity", Integer.class); by hand is tedious and slightly error prone, and because your refactoring tool probably isn't smart enough to change the string based name when you refactor the name of the attribute of the persistent class. Code generation tools don't make these kind of errors, and they don't mind re-doing their work from scratch each time you ask them to.

In a nutshell: the tool uses the non-typesafe metamodel API to build a typesafe metamodel.

The most exciting possibility is that this code generation tool could be an APT plugin for javac. You wouldn't have to run the code generator explicitly, since APT is now fully integrated into the Java compiler. (Or, it could be an IDE plugin.)

But didn't code generation tools go out of fashion recently? Wasn't one of the great features of ORM solutions like Hibernate and JPA that they didn't rely upon code generation? Well, I'm a great believer in using whatever tool is the right solution to the problem at hand. Code generation has certainly been applied to problems where it wasn't the best solution. On the other hand, I don't see anyone bashing ANTLR or JavaCC for their use of code generation to solve the problem they address. In this case, we're working around a specific problem in the Java type system: the lack of a typesafe metamodel (reflection is one of the worst-designed language features). And code generation is simply the only solution that works. Indeed, for this problem it works well.

Don't worry, the generated code won't be hard to understand ... it might look something like this, for example:

public class persistent {
	static Metamodel metaModel;

	public static Entity<model.Order> order = metaModel.entity(model.Order.class);
	public static class Order {
		public static Attribute<model.Order, Long> id = order.id(Long.class);
		public static Set<model.Order, model.Item> items = order.set("items", model.Item.class);
		public static Attribute<model.Order, Boolean> filled = order.att("filled", Boolean.class);
		public static Attribute<model.Order, Date> date = order.att("date", Date.class);
	}

	public static Entity<model.Item> item = metaModel.entity(model.Item.class);
	public static class Item {
		public static Attribute<model.Item, Long> id = item.id(Long.class);
		public static Attribute<model.Item, model.Product> product = item.att("product", model.Product.class);
		public static Attribute<model.Item, model.Order> order = item.att("order", model.Order.class);
		public static Attribute<model.Item, Integer> quantity = item.att("quantity", Integer.class);
	}

	public static Entity<model.Product> product = metaModel.entity(model.Product.class);
	public static class Product {
		public static Attribute<model.Product, Long> id = product.id(Long.class);
		public static Set<model.Product, model.Item> items = product.set("items", model.Item.class);
		public static Attribute<model.Product, String> description = product.att("description", String.class);
		public static Attribute<model.Product, BigDecimal> price = product.att("price", BigDecimal.class);
	}

}

This class just let's us refer to attributes of the entities easily. For example, we could type persistent.Order.id to refer to the id attribute of Order. Or persistent.Product.description to refer to the description of the Product.

back to top