A frequently encountered doubt people have when using jOOQ is to decide when a “complex” query should be written using jOOQ API vs. when it should be implemented using native SQL.
The jOOQ manual is full of side by side examples of the same query, e.g.
ctx.select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, count()) .from(AUTHOR) .join(BOOK).on(AUTHOR.ID.eq(BOOK.AUTHOR_ID)) .groupBy(AUTHOR.ID, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME) .fetch();
Using native SQL:
SELECT author.first_name, author.last_name, COUNT(*) FROM author JOIN book ON author.id = book.author_id GROUP BY author.id, author.first_name, author.last_name;
- Simpler bind values
- Templating for dynamic text based SQL
- All the mapping utilities
- The transaction API or R2DBC support
With jOOQ, you’d then write:
ctx.fetch( """ SELECT author.first_name, author.last_name, COUNT(*) FROM author JOIN book ON author.id = book.author_id GROUP BY author.id, author.first_name, author.last_name """ );
The obvious pros and cons
First off, there are some obvious pros and cons of using jOOQ in any given setting.
You’ll hardly find anything better than jOOQ:
- When your SQL is dynamic
- When you have to support multiple dialects simultaneously (your product supports multiple RDBMS) or consecutively (you might have to switch RDBMS eventually). An example illustrating the many differences between dialects: supporting SQL/JSON
- When you like jOOQ’s type safety via code generation and mapping
- When you have to fetch complex trees of data
- When you have to runtime transform your SQL in various ways
- When you have to bind to stored procedures and functions, again via code generation
- When you’re worried about SQL injection
- When you have to export data
- When you want to run diagnostics and linting on your runtime SQL
All the pros are explained in the articles linked from the above links, so I won’t repeat the benefits here anymore.
jOOQ can get in the way occasionally:
- When you have large static queries using tons of Common Table Expressions (CTE) and derived tables
- When you like to develop your query in a SQL editor instead of your Java IDE
Let’s quickly look at these two items.
- CTE and derived tables have to be declared up front in jOOQ, rather than embedding them in the query. This means that in most cases, there’s no easy way to keep jOOQ’s usual type safety working, and you’re back to composing queries using string identifiers. When the query is dynamic, this approach is still very strong. But when it’s static, then jOOQ may appear to be causing more usability issues than solving problems.
- While it is totally possible to follow a test driven development (TDD) approach to developing your jOOQ queries purely in Java, and running them preferably on testcontainers as we’ve described here, it may well be that you’re more at ease writing your SQL query in native SQL, e.g. in Dbeaver or any other SQL editor of your choice. In that case, you’d have to translate your completed query to jOOQ once it works. We do offer an automated translation service (use the “Java dialect”), but that may still not feel “right,” especially when you have to edit the query again later.
The best of both worlds
jOOQ won’t try to make you use jOOQ where it doesn’t fit. In a previous article, we’ve already elaborated when an ORM (implementing object graph persistence) works better. In this case, we’re discussing pure SQL, where jOOQ shines compared to ORMs, but it may not be right for certain “kinds of SQL queries.”
Those kinds are complex static queries. From a jOOQ perspective, you can get very very far, unless you’re using some really advanced vendor specific SQL feature, like the Oracle
MODEL clause. But these things are a matter of taste. What jOOQ recommends you do in case you feel a certain query is too complex for jOOQ is to either:
Or maybe even better, extract its logic into:
- A SQL view
- A SQL table valued function
Views are supported by all major RDBMS and very underappreciated. Table valued functions can be even more composable, as they take arguments, and can even be inlined by some RDBMS (e.g. SQL Server). In both cases, you get to keep your native SQL syntax, but at the same time, you profit from type safety as the objects are compiled by the RDBMS.
Plus, as I’ve mentioned before, we really recommend you follow a TDD approach to developing your application, using integration tests. If you do that, then adding views and table valued functions to your schema using Flyway or Liquibase or any other means of database change management will be straightforward.
After the change is applied, you’ll regenerate your jOOQ code, and you can immediately use the new schema object in your Java application without any loss of type safety.
Using such a pragmatic approach, you can get the best of both the jOOQ and the native SQL worlds.