jOOQ - A bit different ORM framework
Original name |
jOOQ - tak trochu jiný ORM framework |
Author(s) |
Leoš Přikryl |
Length |
45:50 |
Date |
13-11-2023 |
Language |
Czech 🇨🇿 |
Rating |
⭐⭐⭐⭐☆ |
-
✅ Amazing introduction to jOOQ so I would love to try it out immediately. GitHub copilot.
-
⛔ The transaction management (commits, rollbacks) was not explained at all as well as triggers, procedures, etc. though the session was more than rich enough.
"OpenSource version is for free for the latest open source DB versions (PostgreSQL, MySQL…). It’s a one-man-show project."
jOOQ /ʤuk/
stands for "jOOQ Object Oriented Querying" and is a Java framework for database integration introducing type-safe and database platform-independent SQL queries.
It does not hide the SQL queries, unlike Hibernate, it only makes them easier to use.
SELECT last_name, first_name FROM user
WHERE role = 'admin'
ORDER BY last_name, first_name;
dslc.select(USER.LAST_NAME, USER.FIRST_NAME)
.from(USER)
.where(USER.ROLE.eq("admin"))
.orderBy(USER.LAST_NAME, USER.FIRST_NAME);
Database integration solutions comparison
JPA/Hibernate
Advantages:
-
Database platform-independent SQL.
-
Suitable for CRUD operations and persisting complex object graphs.
Disadvantages:
-
Extremely complex, difficult, and error-prone (lazy fetch, 1-N queries…).
-
Not suitable for complex queries across multiple tables and the following need to be used: JPQL/HQL, Criteria API, QueryDSL (though we know how the SQL should look like, it’s not easy to write it with QueryDSL), native queries…
-
Cannot be used if the database structure is not known in advance.
JDBC
Advantages:
-
Enables the full power of the SQL: useful if the database structure is not known in advance.
Disadvantages:
-
Low-level API that is not database platform-independent and the SQL queries must follow a dialect.
-
No type-safety and prone to SQL injection.
Approaches
Database-first
The database is a source of truth and the code is generated from it. This approach is preferred as the database changes less often than the code.
Developers need to design the data structure and the database becomes more efficiently used.
A code generator produces a Java/Kotlin type-safe code with the database description: tables, sequences, stored procedures, indexes, records
Type safety:
dslc.select(USER.LAST_NAME, USER.FIRST_NAME)
.from(USER)
.where(USER.ROLE.eq(1)) // the USER.ROLE is of the type VARCHAR → compiler-time error
dslc.select(USER.LAST_NAME, USER_FIRSTNAME) // the USER.LAST_NAME was renamed to USER.SURNAME → compiler-time error
.from(USER)
.where(USER.ROLE.eq("admin"));
Generator-first
It’s possible to use jOOQ even without the code generator, though it is not recommended.
It’s useful in case the database structure is not known in advance.
No type safety:
dslc.select(field("last_name"), field("first_name"))
.from(table("user"))
.where(field("role").eq("admin"))
.orderBy(field("last_name"), field("first_name"));
Quick start
Start with the [DSL
](https://www.jooq.org/javadoc/dev/org.jooq/org/jooq/impl/DSL.html) to get DSLContext
which is the main object for writing the queries.
var dslc = DSL.using(connection, SQLDialect.POSTGRES);
var dslc = DSL.using(dataSource, SQLDialect.POSTGRES);
Spring Boot offers org.springframework.boot:spring-boot-starter-jooq
to configure and inject the context.
Queries
WHERE
dslc.select(AUTHOR.LAST_NAME, AUTHOR.FIRST_NAME)
.from(AUTHOR)
.where(
(AUTHOR.LAST_NAME.eq("London").or(AUTHOR.FIRST_NAME.like("J%"))
.and(AUTHOR.YEAR_OF_BIRTH.between(1800, 1900))
);
dslc.select(AUTHOR.LAST_NAME, AUTHOR.FIRST_NAME)
.from(AUTHOR)
.where(
AUTHOR.LAST_NAME.eq("London").or(AUTHOR.FIRST_NAME.like("J%")),
AUTHOR.YEAR_OF_BIRTH.between(1800, 1900)));
JOIN
Join is supported as well as all common joins (INNER
, LEFT/RIGHT/FULL OUTER
, CROSS
) and introduces semi and anti joins.
dslc.select(AUTHOR.LAST_NAME, AUTHOR.FIRST_NAME, BOOK.TITLE)
.from(AUTHOR)
.join(BOOK).on(AUTHOR.ID.eq(BOOK.AUTHOR_ID))
.where(AUTHOR.ID.eq(1));
dslc.select(AUTHOR.LAST_NAME, AUTHOR.FIRST_NAME, BOOK.TITLE)
.from(AUTHOR)
.join(BOOK).onKey() // in case the foreign key is well-configured
.where(AUTHOR.ID.eq(1));
dslc.select(BOOk.author().LAST_NAME, AUTHOR.author().FIRST_NAME, BOOK.TITLE) // jOOQ can do an implicit JOIN
.from(BOOK)
.where(AUTHOR.ID.eq(1));
Semi-JOIN
Semi-join returns all the left-side rows for which there exists at least one right-sided row. It’s a kind of "fake" join as it finds out whether there is something to join.
dslc.select(BOOK.TITLE).from(BOOK)
.leftSemiJoin(BOOK_TO_BOOK_STORE).onKey();
SELECT book.title FROM BOOK
WHERE EXISTS (
SELECT 1 FROM book_to_book_store
WHERE book_to_book_store.book_id = book.id)
// alternative using jOOQ without the semi-join
dslc.select(BOOK.TITLE).from(BOOK)
.whereExists(
dslc.selectOne()
.from(BOOK_TO_BOOK_STORE)
.where(BOOK_TO_BOOK_STORE.BOOK_ID.eq(BOOK.ID)));
Aggregation
dslc.select(count()).from(BOOK);
dslc.select(avg(BOOK.YEAR_OF)).from(BOOK);
dslc.select(LANGUAGE.DESCRIPTION, count())
.from(BOOK).join(LANGUAGE).onKey()
.gropuBy(LANGUAGE.DESCRIPTION)
.orderBy(count()).desc();
Complex queries
SELECT author.first_name, author.last_name, count(*) FROM author
JOIN book ON book.author_id = author.id
JOIN language ON book.language_id = language.id
WHERE lanuage.code = 'CZ' AND book.published_in > DATE '2020-01-01'
GROUP BY author.first_name, author.last_name
HAVING count(*) > 5
ORDER BY author.last_name ASC NULLS FIRST
LIMIT 5 OFFSET10;
dslc.select(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, count()).from(AUTHOR)
.join(BOOK).onKey()
.join(LANGUAGE).onKey()
.where(LANGUAGE.CODE.eq("CZ").and(BOOK.PUBLISHED_IN.gt(LocalDate.of(2020, 1, 1))))
.groupBy(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
.having(count().gt(5))
.orderBy(AUTHOR.LAST_NAME.asc().nullsFirst())
.limit(5).offset(10);
Reading
Single line: fetchOne
returns one or null
or exception in case of more rows, fetchSingle
returns exactly one row or exception in case of more rows, fetchAny
returns one or null
and ignores the rest in case of more rows.
Multiple lines: fetch
returns a List
, fetchLazy
returns a cursor (similar to JDBC ResultSet
), stream
returns Java 8 Stream, fetchGroups
, fetchMap
, fetchArray
…
Writing
Using SQL
dslc.insertInto(AUTHOR, AUTHOR.FIRST_NAME, AUTHOR.LAT_NAME)
.values("Jack", "London")
.execute();
dslc.update(AUTHOR)
.set(AUTHOR.FIRST_NAME, "Jack")
.where(AUTHOR.ID.eq(3))
.execute();
dslc.delete(AUTHOR)
.where(AUTHOR.ID.eq(100))
.execute();
Using record pattern
// CREATE
var book = dslc.newRecord(BOOK);
book.setTitle("1984");
book.store();
// ID is autogenerated and populated automatically
var bookId = book.getId();
// RETRIEVE
var book2 = dslc.fetchSingle(BOOK, BOOK.ID.eq(bookId));
// UPDATE
book2.setPublishedIn(2000);
book2.store();
// DELETE
book2.delete();
Complex features
Emulation of non-existing database functions
For example, PostgreSQL doesn’t know median
.
SELECT percentile_cont(0.5) WITHING GROUP (ORDER BY book.published_in) FROM book;
dslc.select(median(BOOK.PUBLISHED_IN)).from(BOOK); // there is no PostgreSQL median
Enumerations
Database-native enums can be mapped to the Java enum
. Hibernate struggles with PostgreSQL enumerations.
CREATE TYPE book_type AS ENUM ('NOVEL', 'POEM', 'ESSAY'); -- PostgreSQL enumerations
dslc.celect(BOOK.TITLE).from(BOOK).where(BOOK.TYPE.eq(BookTypeEnum.NOVEL));
Native database arrays
Database-native arrays can be mapped to the Java array.
CREATE TABLE book (
tags text[]
);
public class Book implements Serializable { // jOOQ generated class
private String[] tags;
// getters and setters
}
Nested data structures
They are supported in jOOQ, though Hibernate excels in it.
Sadly, only H2 and PostgreSQL database support native arrays (the method array
can be used), though in other database types the method multiset
is a proper substitution for array
.
public record Book(int id, String title) {}
public record Name(String first, String last) {}
public record Author(int id, Name name, Book[] books) {}
// this generates duplicated author first and surnames and requires further processing in Java
dslc.select(AUTHOR.ID, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME, BOOK.ID, BOOK.TITLE)
.from(AUTHOR)
.join(BOOK).onKey()
.fetch();
// column mapping
dslc.select(
AUTHOR.ID,
row(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME).as("name"))
array(
dslc.select(row(BOOK.ID, BOOK.TITLE))
.from(BOOK)
.where(BOOK.AUTHOR_ID.eq(AUTHOR_ID))).as("books"))
.from(AUTHOR)
.fetchInto(Author::class);
// constructor mapping
dslc.select(
AUTHOR.ID,
row(AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME).mapping(Name::new))
array(
dslc.select(row(BOOK.ID, BOOK.TITLE))
.mapping(Book::class, Book::new)
.from(BOOK)
.where(BOOK.AUTHOR_ID.eq(AUTHOR_ID)))
.from(AUTHOR)
.fetchInto(Author::class);
Format as JSON
var jsonFormat = new JSONFORMAT()
.recordFormat(JSONFormat.RecordFormat.OBJECT)
.format(true);
System.out.println(query.fetch().formatJSON(jsonFormat));
Format as an ASCII table
It is useful for debugging due to the well-overridden Object#toString
method.
System.out.println(query.fetch());
Other features
The main use case is for a dynamic approach to the database, it can generate procedures, triggers, etc. It has a rich API over the databases.
Migration scripts are recommended to use and work well with jOOQ.
Works well with Hibernate and those can be combined over the same transaction manager. This approach is also recommended to combine various use cases where each framework excels.
jOOQ dual license model
-
OpenSource version is for free for the latest open source DB versions (PostgreSQL, MySQL…).
-
Paid version (99€, 399€, and 999€ per developer per year depending on the obscurity of the database) has no further fees (application, server) and supports older open source database versions as well as other non-open source databases. The paid subscription includes support.
Credits
Author of jOOQ is Lukas Eder in his Data Geekery company in Switzerland. Sadly, it’s a one-man-show project but has a nice guide and a blog.
GitHub Copilot can generate some easy jOOQ code from SQL.