The Context
In this post I want to dive into a significant shift in my persistence strategy. For a P.o.C. project, I have decided to utilize jOOQ for the first time… and the experience has been a revelation. One of the most exhausting aspects of traditional database interaction is the maintenance of “native queries” or bulky JPA abstractions that often hide the underlying SQL logic. The jOOQ solves this by being type-safe and generating SQL oriented directly towards data, allowing the use of native database functions without the heavy overhead of standard ORMs.
The Problem
My history with MyBatis goes back many years, to a time when I was working for a bank and using it meant struggling with an incredibly complex configuration of XML files and external mappers that were a nightmare to maintain. Returning to it in 2026 has been a pleasant surprise: the transition to annotations has made the framework much simpler and cleaner. By using annotations like @Mapper and @Insert directly on Java interfaces, we eliminate the verbosity of the past while retaining the framework’s core strength: direct control over SQL execution.
The Solution
Important Disclaimer: This is a Proof of Concept (PoC) built for a specific course need. If you find this code useful and decide to use it, you do so entirely at your own risk. The authors decline any responsibility for its use.
A key takeaway from this implementation is the good practice of using MyBatis for persistence (writes) and jOOQ for queries (reads). This “dual implementation” allows us to use MyBatis for fast, straightforward inserts and updates, while leveraging jOOQ’s DSL for complex, type-safe filtering and data retrieval. This separation of concerns ensures that our write operations remain performant and simple, while our read operations are flexible and protected by compile-time checks.
Before we dive in, the full source code is available on GitHub: 👉 https://github.com/gabo-gil-playground/java-poc-oauth-jqoo-openai
To get started, your build.gradle needs the right starters to enable both frameworks alongside the PostgreSQL driver. We also include the jOOQ codegen plugin to ensure our Java classes stay in sync with the database schema. This setup ensures that Spring Boot can auto-configure the DSLContext for jOOQ while managing the MyBatis mappers under a unified transaction manager:
dependencies {
implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:4.0.1")
implementation("org.springframework.boot:spring-boot-starter-jooq") // jOOQ 3.19.30
runtimeOnly("org.postgresql:postgresql")
}
Pro-tip #1: The @Insert annotation
In the persistence layer, the MyBatis mappers are now concise. For instance, creating a blog request only requires a simple interface with an @Insert annotation, utilizing Java Records for data transfer. This approach keeps the boilerplate to a minimum while providing clear entry points for data modification.
@Mapper
public interface BlogRequestMapper {
@Insert("INSERT INTO BLOG_REQUEST(title, status) VALUES(#{title}, #{status})")
@Options(useGeneratedKeys = true, keyProperty = "id")
void insert(BlogRequestRecord record);
}
Pro-tip #2: Perform type-safe queries
The retrieval logic resides in BlogSummarizeService.java. Here, we inject the DSLContext to perform type-safe queries. Using jOOQ’s fetchInto capability, we can map database results directly into DTOs like BlogSummarizeRow with minimal effort. This service effectively bridges the gap between our stored data and the API requirements:
@Service
public class BlogSummarizeService {
private final DSLContext dsl;
public List getSummaries() {
return dsl.select(field("BLOG_REQUEST_ID"), field("SUMMARIZE"))
.from(table("BLOG_REQUEST"))
.fetchInto(BlogSummarizeRow.class);
}
}
To run the code, you just need to run gradle bootRun via your terminal or favorite IDE (I personally recommend Intellij). For more details, please, check the README.md file.
Summary of Steps
- Define your dependencies.
- Configure jOOQ plugin to read DDL objects from the Database – pro tip #3: avoid hardcoded values and use environment variable to read DB configuration values like user, password or host.
- Create MyBastis mapper(s) for persistence layer.
- Inject the DSLContext into your service(s) or data access implementation to perform type-safe queries by jOOQ.
The Results
Ultimately, this hybrid approach provides a “turnkey” solution for modern Java applications. By integrating MyBatis and jOOQ we benefit from the best of both worlds: the simplicity of annotated mappers for data entry and the robust, type-safe power of a DSL for data extraction. This architecture is not just cleaner to read but also significantly easier to maintain as the project scales.
Want to dive deeper into any of these concepts? Drop a comment below and let me know which topic you’d like us to explore in our next Hands-on series!
Do you need help modernizing your legacy applications, optimizing your architecture or improving your development workflows? Let’s connect on LinkedIn or check out my services on Upwork.
Leave a Reply