When using jOOQ to create dynamic SQL statements (one of jOOQ’s core value propositions), it is often necessary to add query elements conditionally, with a default “No-op” behaviour. For first time users, this default “no-op” behaviour is not always obvious as the jOOQ API is vast, and as with any vast API, there are many different options to do similar things.
How not to do it
A common pitfall is to be tempted to work with the many XYZStep
types. What types are these? They are usually invisible to the developer as developers use jOOQ’s DSL API in a fluent fashion, just like the JDK Stream API. For example:
DSLContext ctx = ...; Result<?> result = ctx.select(T.A, T.B) .from(T) .where(T.C.eq(1)) .and(T.D.eq(2)) .fetch();
Let’s decompose the above query to see what happens in the API. We could assign every method result to a local variable:
SelectFromStep<?> s1 = ctx.select(T.A, T.B); SelectWhereStep<?> s2 = s1.from(T); SelectConditionStep<?> s3 = s2.where(T.C.eq(1)); SelectConditionStep<?> s4 = s3.and(T.D.eq(2)) Result<?> result = s4.fetch();
Our previous fluent API design blog post explains this API design technique.
This is not what people usually do with “static SQL” statements, but they might be tempted to do this if they wanted to add the last predicate (T.D = 2
) conditionally, e.g:
DSLContext ctx = ...; SelectConditionStep<?> c = ctx.select(T.A, T.B) .from(T) .where(T.C.eq(1)); if (something) c = c.and(T.D.eq(2)); Result<?> result = c.fetch();
This is perfectly valid API usage, but we do not recommend it because it is very messy and leads to difficult to maintain client code. Also, it is absolutely unnecessary, because there is a better way:
Composing queries from its parts
The problem with the above approach is that it is trying to use an imperative approach of adding things to a query step by step. This is how many developers tend to structure their code, but with SQL (and by consequence, jOOQ) that can turn out to be difficult to get right. A functional approach tends to work better.
Notice that not only the entire DSL structure could be assigned to local variables, but also the individual SELECT
clause arguments. For example:
DSLContext ctx = ...; List<SelectField<?>> select = Arrays.asList(T.A, T.B); Table<?> from = T; Condition where = T.C.eq(1).and(T.D.eq(2)); Result<?> result = ctx.select(select) .from(from) .where(where) .fetch();
In fact, every jOOQ query is a dynamic SQL query. Many queries just happen to look like static queries, due to jOOQ’s API design.
Again, we wouldn’t be assigning every SELECT
clause argument to a local variable, only the truly dynamic ones. For example:
DSLContext ctx = ...; Condition where = T.C.eq(1); if (something) where = where.and(T.D.eq(2)); Result<?> result = ctx.select(T.A, T.B) .from(T) .where(where) .fetch();
This already looks quite decent.
Avoid breaking readability
A lot of people aren’t happy with this approach either, because it breaks a query’s readability by making its components non-local. The predicates in the query are declared up front, away from the query itself. This isn’t how many people like to reason about SQL.
And you don’t have to! It is totally possible to embed the condition directly in the WHERE
clause like this:
DSLContext ctx = ...; Result<?> result = ctx.select(T.A, T.B) .from(T) // We always need this predicate .where(T.C.eq(1)) // This is only added conditionally .and(something ? T.D.eq(2) : DSL.noCondition()) .fetch();
The magic is in the above usage of DSL.noCondition
, which is a pseudo predicate that does not generate any content. It is a placeholder where an org.jooq.Condition
type is required without actually materialising one.
There is also:
DSL.trueCondition
:TRUE
or1 = 1
in SQL, the identity forAND
operation reductions.DSL.falseCondition
:FALSE
or1 = 0
in SQL, the identity forOR
operation reductions
… but that requires having to think about these identities and the reductions all the time. Also, if you append many of these trueCondition()
or falseCondition()
to a query, the resulting SQL tends to be quite ugly, for example for people having to analyse performance in production. noCondition()
just never generates any content at all.
Note that
noCondition()
does not act as an identity! If yournoCondition()
is the only predicate left in aWHERE
clause, there will not be anyWHERE
clause, regardless if you work withAND
predicates orOR
predicates.
No-op expressions in jOOQ
When using dynamic SQL like this, and adding things conditionally to queries, such “no-op expressions” become mandatory. In the previous example, we’ve seen how to add a “no-op predicate” to a WHERE
clause (the same approach would obviously work with HAVING
and all other clauses that work with boolean expressions).
The three most important jOOQ query types are:
org.jooq.Field
: for column expressionsorg.jooq.Condition
: for boolean column expressions / predicatesorg.jooq.Table
: for table expressions
Users may want to add all of these conditionally to queries.
org.jooq.Condition
We’ve already seen how to do this with org.jooq.Condition
.
org.jooq.Field
What about dynamic column expressions in the projection (the SELECT
clause)? Assuming you want to project columns only in certain cases. In our example, the T.B
column is something we don’t always need. That’s easy! The same approach can be used (assuming T.B
is a string column):
DSLContext ctx = ...; Result<Record2<String, String>> result = ctx.select(T.A, something ? T.B : DSL.inline("").as(T.B)) .from(T) .where(T.C.eq(1)) .and(T.D.eq(2)) .fetch();
Using inlined parameters via DSL.inline(), you can easily produce a no-op value in your projection if you don’t want to modify the projection’s row type. The advantage is that you can now use this subquery in a union that expects two columns:
DSLContext ctx = ...; Result<Record2<String, String>> result = // First union subquery has a conditionally projected column ctx.select(T.A, something ? T.B : DSL.inline("").as(T.B)) .from(T) .where(T.C.eq(1)) .and(T.D.eq(2)) .union( // Second union subquery has no such conditions select(U.A, U.B) .from(U)) .fetch();
You can take this one step further, and make an entire union subquery conditional this way!
DSLContext ctx = ...; Result<Record2<String, String>> result = // First union subquery has a conditionally projected column ctx.select(T.A, T.B) .from(T) .union( something ? select(U.A, U.B).from(U) : select(inline(""), inline("")).where(falseCondition()) ) .fetch();
This is a bit more syntactic work, but it’s nice to see how easy it is to add something conditionally to a jOOQ query without making the query completely unreadable. Everything is local to where it is being used. No local variables are needed, no imperative control flow is invoked.
And because everything is now an expression (and not a statement / no control flow), we can factor out parts of this query into auxiliary methods, that can be made reusable.
org.jooq.Table
Conditional table expressions usually appear when doing conditional joins. This is usually not done in isolation, but together with other conditional elements in a query. E.g. if some columns are projected conditionally, those columns may require an additional join, as they originate from another table than the tables that are used unconditionally. For example:
DSLContext ctx = ...; Result<?> result = ctx.select( T.A, T.B, something ? U.X : inline("")) .from( something ? T.join(U).on(T.Y.eq(U.Y)) : T) .where(T.C.eq(1)) .and(T.D.eq(2)) .fetch();
There isn’t a more simple way to produce the conditional JOIN
expression, because JOIN
and ON
need to be provided independently. For simple cases as shown above, this is perfectly fine. In more complex cases, some auxiliary methods may be needed, or views.
Conclusion
There are two important messages here in this post:
- The XYZStep types are auxiliary types only. They are there to make your dynamically constructed SQL statement look like static SQL. But you should never feel the need to assign them to local variables, or return them from methods. While it is not wrong to do so, there is almost always a better way to write dynamic SQL.
- In jOOQ, every query is a dynamic query. This is the benefit of composing SQL queries using an expression tree like the one that is used in jOOQ’s internals. You may not see the expression tree because the jOOQ DSL API mimicks static SQL statement syntax. But behind the scenes, you’re effectively building this expression tree. Every part of the expression tree can be produced dynamically, from local variables, methods, or expressions, such as conditional expressions. I’m looking forward to using the new JEP 361 switch expressions in dynamic SQL. Just like a SQL
CASE
expression, some SQL statement parts can be constructed dynamically in the client, prior to passing them to the server.
Once these two things are internalised, you can write very fancy dynamic SQL, including using FP approaches to constructing data structures, such as a jOOQ query object.