Custom Java query class (DSL): Builder pattern, static imports or something else for complex queries?

Alp picture Alp · Mar 30, 2012 · Viewed 8.5k times · Source

I am creating a custom query class, and i am unsure about the most elegant way to code it.

The goals are:

  • Easy to use
  • Extensibility
  • Flexible so that complex queries can be formulated

Approaches

Currently i can think of two alternatives.

1. Builder pattern

Result r = new Query().is("tall").capableOf("basketball").name("michael").build();

The methods is(), capableOf() and name() return a self-reference to the Query object. build() will return a Result object.

2. Static Imports

Result r = new Query(is("tall"), capableOf("basketball"), name("michael"));

The methods is(), capableOf() and name() are static imports and return Condition objects. The Query constructor takes an arbitrary number of conditions and returns the result.

And/Or/Not queries

More complex queries like the following are complicated to formulate:

tall basketball player named [michael OR dennis]

UNION

silver spoon which is bent and shiny

Builder pattern:

Result r = new Query().is("tall").capableOf("basketball").or(new Query().name("michael"), new Query().name("dennis")).
    union(
        new Query().color("silver").a("spoon").is("bent").is("shiny")
    ).
    build();

This is difficult to write and read. Also, i do not like the multiple use of new.

Static imports:

Result r = new Query(is("tall"), capableOf("basketball"), or(name("michael"), name("dennis"))).
    union(color("silver"), a("spoon"), is("bent"), is("shiny"));

Looks better to me, but i do not really like the use of static imports. They are difficult in terms of ide integration, auto-completion and documentation.

Sum up

I am looking for an effective solution, therefore i am open to suggestions of any kind. I am not limited to the two alternatives i presented, if there are other possibilities i'd be happy if you tell me. Please inform me if you need further information.

Answer

Lukas Eder picture Lukas Eder · Mar 30, 2012

You are about to implement a domain specific language (DSL) in Java. Some would refer to your DSL as being an "internal" DSL, because you want to use standard Java constructs for it as opposed to "external" DSLs, which are much more powerful (SQL, XML, any type of protocol), but have to be constructed primitively using string concatenation.

Our company maintains jOOQ, which models SQL as "internal" DSL in Java (this was also mentioned in one of the comments). My recommendation for you is that you follow these steps:

  1. Become aware of what your language should look like. Don't think in terms of Java ("internal" DSL) right away. Think in terms of your very own language ("external" DSL). The fact that you will implement it in Java should not be important at that point. Maybe you'll even implement it in XML, or you'll write your own parser/compiler for it. Thinking about your language specification first, before implementing it in Java will make your DSL more expressive, more intuitive, and more extensible.
  2. Once you've settled for the general syntax and semantics of your language, try drawing a BNF notation of your language. You don't have to be overly precise at the beginning, but this will give it some formal aspects. Railroad diagrams is a very nice tool for that. You will become aware of which combinations are possible and which ones aren't. Also, it is a good way to create an overall language documentation, because single-method Javadocs won't be much help to your newbie users.
  3. When you have a formal syntax, follow the rules that we have mentioned in our blog here: http://blog.jooq.org/2012/01/05/the-java-fluent-api-designer-crash-course. These rules have proven very useful when designing the jOOQ API, which has been reported by our users to be very intuitive (if they already know SQL, that is).

My personal recommendation for you is this:

  1. is, has, capableOf, etc are predicate factory methods. Static methods are your best choice in Java, because you will probably want to be able to pass predicates to various other DSL methods of your API. I don't see any problem with IDE integration, auto-completion, or documentation, as long as you put them all in the same factory class. Specifically Eclipse has nice features for that. You can put com.example.Factory.* to your "favourites", which leads to all methods being available everywhere from the auto-completion dropdown (which is again a good access-point for Javadocs). Alternatively, your user can just static-import all methods from the Factory wherever they need it, which has the same result.
  2. and, or, not should be methods on the predicate type (not may also be a central static method). This leads to an infix notation for boolean combinations, which is considered more intutitve by many developers, than what JPA/CriteriaQuery did:

    public interface Predicate {
    
      // Infix notation (usually a lot more readable than the prefix-notation)
      Predicate and(Predicate... predicate);
      Predicate or(Predicate... predicate);
    
      // Postfix notation
      Predicate not();
    
      // Optionally, for convenience, add these methods:
      Predicate andNot(Predicate... predicate);
      Predicate orNot(Predicate... predicate);
    }
    
    public class Factory {
    
      // Prefix notation
      public static Predicate not(Predicate predicate);
    }
    
  3. For unions, you have several options. Some examples (which you can also combine):

    // Prefix notation
    public class Factory {
      public static Query union(Query... queries);
    }
    
    // Infix notation
    public interface Query {
      Query union(Query... queries);
    }
    
  4. Last but not least, if you want to avoid the new keyword, which is part of the Java language, not of your DSL, do also construct queries (the entry points of your DSL) from a Factory:

    // Note here! This is your DSL entry point. Choose wisely whether you want
    // this to be a static or instance method.
    // - static: less verbose in client code
    // - instance: can inherit factory state, which is useful for configuration
    public class Factory {
    
      // Varargs implicitly means connecting predicates using Predicate.and()
      public static Query query(Predicate... predicates);
    
    }
    

With these examples, you can construct queries as such (your example):

tall basketball player named [michael OR dennis]

UNION

silver spoon which is bent and shiny

Java version:

import static com.example.Factory.*;

union(
  query(is("tall"), 
        capableOf("basketball"), 
        name("michael").or(name("dennis"))
  ),
  query(color("silver"),
        a("spoon"),
        is("bent"),
        is("shiny")
  )
);

For further inspiration, have a look at jOOQ, or also at jRTF, which also does an excellent job at modelling RTF ("external" DSL) in Java as an "internal" DSL