I use this to illustrate how to implement an internal DSL. It is not hard once you have learned the tools to play with. Java is not optimal for DSLs, but let us use it as good as possible.
and a more advanced criteria:
But that is not very good because it allows invalid expressions, such
This also facilitates code completion (ctrl+space) in a good way, which was one of our goals.
The goal was to create a fluent api that supported code completion and refactoring in a good way. A simple criteria may look like:
criteriaFor(Person.class).withProperty(sex()).eq(Gender.FEMALE).build();
and a more advanced criteria:
I will show how a few pieces of the DSL was developed. I have mostly used Expression Builder and Method Chaining, but also a few other tricks.
criteriaFor(Person.class)
.withProperty(sex()).eq(Gender.FEMALE).and()
.withProperty(ssn().country()).eq(SWEDEN).and()
.lbrace().withProperty(name().first()).like("A%")
.or().withProperty(name().last()).like("A%").rbrace()
.orderBy(name().last())
.build();
The expression builder doesn't create the JPA/Hibernate criteria directly. It creates an intermediate structure of objects , which can be translated to criteria API. The main reason for that is decoupling and possibility to have vendor specific implementations.
First, let me state the obvious: When developing an internal DSL it is perfect to do it the TDD way. You will add more and more features and do a lot of refactoring until you are satisfied with the language.
Enough of the introductory talking, let us go hands on to a simple equals expression:
List<ConditionalCriteria> conditionalCriteria =This can be implemented in the builder as:
ConditionalCriteriaBuilder.criteria().prop("name").eq("Svensson").build();
public class ConditionalCriteriaBuilder {
private final List<ConditionalCriteria> criteriaList = new ArrayList<ConditionalCriteria>();
private String propertyName;
private ConditionalCriteriaBuilder() {
}
public static ConditionalCriteriaBuilder criteria() {
return new ConditionalCriteriaBuilder();
}
public List<ConditionalCriteria> build() {
return criteriaList;
}
public ConditionalCriteriaBuilder prop(String name) {
propertyName = name;
return this;
}
public ConditionalCriteriaBuilder eq(Object value) {
addCriteria(ConditionalCriteria.equal(propertyName, value));
return this;
}
private void addCriteria(ConditionalCriteria criteria) {
criteriaList.add(criteria);
}
}
But that is not very good because it allows invalid expressions, such
criteria().eq("A").prop("aaa").eq("B").
Therefore I introduce an interface for the property level:
public PropBuilder prop(String name) {
return new PropBuilderImpl(name);
}
public interface PropBuilder {
ConditionalCriteriaBuilder eq(Object value);
}
private class PropBuilderImpl implements PropBuilder {
String propertyName;
PropBuilderImpl(String name) {
this.propertyName = name;
}
public ConditionalCriteriaBuilder eq(Object value) {
addCriteria(ConditionalCriteria.equal(propertyName, value));
return ConditionalCriteriaBuilder.this;
}
}
This also facilitates code completion (ctrl+space) in a good way, which was one of our goals.
In next post, tomorrow, I will take care of the string property names that are not very refactoring friendly.
If you want a typesafe compact alternative to write fluent queries for JPA, then try out Querydsl.
ReplyDelete