Red Hat

Java 6 compiler plugins and typesafe criteria queries

Posted by Gavin King    |       |    Tagged as Hibernate ORM JPA

There's been plenty of discussion in the JPA group about my typesafe criteria proposal. My new favorite feature of the Java language is javax.annotation.Processor. Java 6 annotation processors are derived from the APT tool that existed in JDK 5, but are built into javac. Really, the name annotation processor is misleading, since this feature is only incidentally related to annotations. The Processor is really a fairly general purpose compiler plugin. If, like me, you've never been a fan of code generation, now is the time to reconsider. A Java 6 Processor can:

  • analyze the compiler's metamodel of the Java source code that is being compiled
  • search the source path for other metadata, such as XML
  • generate new types, which will also be compiled, or other files

Best of all, this functionality requires no special tool or commandline options to javac. All you need to do is put the jar containing your Processor in the classpath, and the compiler does the rest!

In the typesafe query API, I want to use this to work around Java's lack of a typesafe metamodel for fields and methods of a class. The basic idea is that the compiler plugin will generate a metamodel type for each persistent class in the application.

Suppose we have the following persistent class:

@Entity
public class Order {
    @Id long id;
	
    boolean filled;
    Date date;

    @OneToMany Set<Item> items;

    @ManyToOne Shop shop;
	
    //getters and setters...
}

Then a class named Order_ would be generated, with a static member of each persistent attribute of Order, that the application could use to refer to the attributes in queries.

After several iterations, we've settled on the following format for the generated type:

import javax.jpa.metamodel.Attribute;
import javax.jpa.metamodel.Set;
import javax.jpa.metamodel.Metamodel;

@Metamodel
public abstract class Order_ {
    public static Attribute<Order, Long> id;
    public static Attribute<Order, Boolean> filled;
    public static Attribute<Order, Date> date;
    public static Set<Order, Item> items;
    public static Attribute<Order, Shop> shop;
}

The JPA provider would be responsible for initializing the values of these members when the persistence unit is initialized.

Now, criteria queries would look like the following:

Root<Item> item = q.addRoot(Item.class);
Path<String> shopName = item.get(Item_.order)
                            .get(Order_.shop)
                            .get(Shop_.name);
q.select(item)
 .where( qb.equal(shopName, "amazon.com") );

Which is equivalent to:

select item 
from Item item
where item.shop.name = 'amazon.com'

Or like:

Root<Order> order = q.addRoot(Order.class);
Join<Item, Product> product = order.join(Order_.items)
		                   .join(Item_.product);
		
Path<BigDecimal> price = product.get(Product_.price);
Path<Boolean> filled = order.get(Order_.filled);
Path<Date> date = order.get(Order_.date);
		
q.select(order, product)
 .where( qb.and( qb.gt(price, 100.00), qb.not(filled) ) )
 .order( qb.ascending(price), qb.descending(date) );

Which is equivalent to:

select order, product 
 from Order order 
    join order.items item
    join item.product product
 where
    product.price > 100 and not order.filled
 order by
    product.price asc, order.date desc

The queries are almost completely typesafe. Because of the generic type parameters of Attribute:

  • I can't pass an attribute of Order to a Join or Path that represents an Item, and
  • I can't try to perform a comparison like gt() on a Path that represents a boolean attribute, or not() on a Path that represents an attribute of type Date.

There's some skeptics in the expert group, but my feeling is that once people get used to the idea that type generation is no longer something that gets in your way during development, we're going to see a lot more frameworks using this kind of approach. I certainly think this API is a big improvement over the previous proposal:

Root item = q.addRoot(Item.class);
Path shopName = item.get("order")
                    .get("shop")
                    .get("name");
q.select(item)
 .where( qb.equal(shopName, "amazon.com") );

Or:

Root order = q.addRoot(Order.class);
Join product = order.join("items")
		    .join("product");
		
Path price = product.get("price");
Path filled = order.get("filled");
Path date = order.get("date");
		
q.select(order, product)
 .where( qb.and( qb.gt(price, 100.00), qb.not(filled) ) )
 .order( qb.ascending(price), qb.descending(date) );

Both these queries are riddled with non-typesafe method invocations which can't be validated without executing the query.

back to top