Type safe property paths using QueryDSL

Property paths are often described as string, which is not type-safe and very error prone. In this blog post I will demonstrate how to replace these string based expressions with type-safe property paths, allowing faulty property paths to be detected at compile time.

Introduction




Paths to bean properties are often described in a string format which is not type-safe and very error prone. Imagine we have the following beans:




public class Car {
private Person owner;
private Integer horsePower;
}

public class Person {
private String name;
}



Then the expression of a car owner would be “owner.name”. Where it is common practice to nest properties with the period ‘.’ character. Many frameworks, such as Spring and Wicket, build on this usage of property expressions, for example:




new Sort("owner.car");



However, the usage of a string based property path is very error prone and faults can be hard to detect. Image that the ‘name’ property gets changed to ‘firstName’, then our sort would no longer work. And because the path is a string, we will not know about this bug until runtime (if there are proper tests)!




Wouldn’t it be nicer to express paths to a bean property entirely type-safe, filtering out invalid property paths at compile time?



QueryDSL




QueryDSL is a framework that can be used to perform type-safe queries. Queries are specified from a type-safe meta model, which can be generated for any type of bean. We could also use this meta model to specify type-safe property paths.


Our generated meta model looks as follows:




public class QCar extends BeanPath<Car> {
public static final QCar car = new QCar(new BeanPath<Car>(Car.class, "car"));
public final QPerson owner = new QPerson(new BeanPath<Person>(Person.class, this, "owner"));
public final NumberPath<Integer> horsePower = createNumber("horsePower", Integer.class);

public QCar(BeanPath<? extends Car> entity) {
super(entity.getType(), entity.getMetadata());
}

public QCar(PathMetadata<?> metadata) {
super(Car.class, metadata);
}
}

public class QPerson extends BeanPath<Person> {
public static final QPerson person = new QPerson(new BeanPath<Person>(Person.class, "person"));
public final StringPath name = createString("name");

public QPerson(BeanPath<? extends Person> entity) {
super(entity.getType(), entity.getMetadata());
}

public QPerson(PathMetadata<?> metadata) {
super(Person.class, metadata);
}
}



So a car owner would be expressed as “QCar.car.owner.name”.




Because the meta model is (re)generated during compilation, it is always kept up to date with our domain beans. Causing faulty paths to be detected at compile time.



Usage




Replacing the string based expression with a type-safe QueryDSL path clearly has many advantages. Below I will demonstrate some examples:



Hamcrest matchers




Car car = new Car().setOwner(new Person("joe"));
assertThat(car, hasValue(QCar.car.owner.name, equalTo("joe")));

Full code snippet here.




Rather than:




assertThat(car, hasProperty("owner", hasProperty("name", equalTo("joe")));



Besides guaranteeing that the property path is valid, we also enforce that the property matcher has to match our property type. Meaning that we can only use string based matchers on the “owner.name” property, where before any type of matcher was allowed.



Sorting




Car car = new Car().setHorsePower(50);
Car betterCar = new Car().setHorsePower(150);
assertEquals(-1, pathComparator(QCar.car.horsePower).compare(car, betterCar));


Full code snippet here.



Clearly the major frameworks, described above, will not let their public API depend on QueryDSL. But it is quite easy to build your own decorator, which translates the property path into a string based expression. That way you can enjoy type safety with little effort.