Java 代码里写 SQL 太痛苦了?JOOQ 类型安全查询——编译期就能发现 SQL 错误
引言
线上事故复盘:
OrderMapper.xml 中有一行 SQL:
SELECT * FROM order WHERE user_id = #{userId}
半夜上线后报错:You have an error in your SQL syntax
原因:`order` 是 MySQL 关键字,应该加反引号 `order`
这种错误你大概率遇到过。MyBatis 把 SQL 写在 XML 里,本质上还是字符串拼接——表名错了、字段名错了、参数类型错了,只有运行时才暴露。
有没有办法在写代码时就发现这些错误?有——用 jOOQ。
jOOQ(Java Object Oriented Querying)把数据库表映射成 Java 类,用 DSL(领域特定语言)写 SQL,编译期就能检查表名、字段名、类型。IDE 自动补全,重构跟着改,错字段直接红线。
本文对比 MyBatis 和 jOOQ,从代码生成到实战 CRUD、复杂联表、动态条件、事务,最后附 Spring Boot 集成配置和常见坑。
一、MyBatis 的痛点:SQL 是字符串
1.1 字符串拼接的代价
<!-- OrderMapper.xml -->
<select id="selectById" resultType="Order">
SELECT id, order_no, user_id, amount, status
FROM orders
WHERE id = #{id}
</select>
看起来清爽,但问题是——这段 SQL 是字符串,编译器看不到它的语义。
1.2 五类运行时才暴露的错误
| 错误类型 | 例子 | 何时暴露 |
|---|---|---|
| 表名拼错 | FORM orders | 运行时 SQL 异常 |
| 字段名拼错 | SELECT oder_no | 运行时 |
| 类型不匹配 | WHERE id = 'abc'(id 是 Long) | 运行时 |
| 关键字冲突 | FROM order | 运行时 |
| 字段重命名后忘改 | user_id 改成 uid,XML 没改 | 运行时 |
1.3 字符串 SQL 的真实代价
场景:把 users 表的 phone 字段重命名为 mobile
MyBatis:
全局搜索 "phone"
找到 20 个 XML 文件涉及
逐一改 → 漏改 3 个 → 上线后部分接口报错
jOOQ:
数据库改完 → 重新生成代码 → 编译
3 个文件直接编译报错 → 改完 → 安全上线
二、JOOQ 是什么
2.1 核心思想
把数据库的 schema 反向工程成 Java 类,每个表对应一个 Table 对象,每个字段对应一个 Field 对象。
数据库 orders 表 Java 类(jOOQ 生成)
┌──────────────┐ ┌──────────────────────┐
│ id BIGINT │ ──────────→ │ ORDERS.ID │
│ order_no │ ──────────→ │ ORDERS.ORDER_NO │
│ user_id │ ──────────→ │ ORDERS.USER_ID │
│ amount │ ──────────→ │ ORDERS.AMOUNT │
│ status │ ──────────→ │ ORDERS.STATUS │
└──────────────┘ └──────────────────────┘
写 SQL 就成了写 Java 代码:
// jOOQ 写法
List<Order> orders = create
.select(ORDERS.ID, ORDERS.ORDER_NO, ORDERS.AMOUNT)
.from(ORDERS)
.where(ORDERS.USER_ID.eq(123L))
.fetchInto(Order.class);
每个字段都是强类型的对象:
ORDERS.USER_ID类型是Field<Long>,传字符串编译失败ORDERS.AMOUNT类型是Field<BigDecimal>,传整数自动转换- 表名重命名?重新生成代码 → 编译报错 → 改完 → 安全
2.2 MyBatis vs JOOQ 对比
| 维度 | MyBatis | jOOQ |
|---|---|---|
| SQL 写在哪 | XML 字符串 | Java DSL |
| 表名/字段错 | 运行时 | 编译期 |
| 字段重命名 | 全局搜索 | 重新生成代码,编译报错 |
| IDE 补全 | 无(XML) | ✅ 完整 |
| 重构支持 | 弱 | ✅ 跟随 Java 重构 |
| 动态 SQL | `` 标签,复杂 | Condition 对象组合 |
| 复杂联表 | 难写难读 | DSL 链式,清晰 |
| 学习曲线 | 低 | 中 |
| 适合场景 | 复杂报表 SQL | 业务 CRUD + 标准查询 |
2.3 jOOQ 的另一个优势:DSL = SQL
jOOQ 的 DSL 几乎和 SQL 语法 1:1 对应:
-- SQL
SELECT id, order_no, amount
FROM orders
WHERE user_id = 123 AND status = 'PAID'
ORDER BY create_time DESC
LIMIT 10
// jOOQ
create.select(ORDERS.ID, ORDERS.ORDER_NO, ORDERS.AMOUNT)
.from(ORDERS)
.where(ORDERS.USER_ID.eq(123L).and(ORDERS.STATUS.eq("PAID")))
.orderBy(ORDERS.CREATE_TIME.desc())
.limit(10)
.fetchInto(Order.class);
会写 SQL 就会用 jOOQ。
三、Spring Boot 集成 jOOQ
3.1 添加依赖
<dependencies>
<!-- jOOQ -->
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.19.10</version>
</dependency>
<!-- Spring Boot jOOQ Starter -->
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-spring-boot-starter</artifactId>
<version>3.19.10</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies>
3.2 代码生成插件
jOOQ 的核心——根据数据库 schema 生成 Java 类:
<build>
<plugins>
<plugin>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen-maven</artifactId>
<version>3.19.10</version>
<executions>
<execution>
<goals><goal>generate</goal></goals>
</execution>
</executions>
<configuration>
<jdbc>
<driver>com.mysql.cj.jdbc.Driver</driver>
<url>jdbc:mysql://localhost:3306/demo</url>
<user>root</user>
<password>root123</password>
</jdbc>
<generator>
<database>
<name>org.jooq.meta.mysql.MySQLDatabase</name>
<includes>.*</includes> <!-- 所有表 -->
<excludes>undo_log</excludes>
<inputSchema>demo</inputSchema>
</database>
<generate>
<pojos>true</pojos> <!-- 生成 POJO -->
<daos>true</daos> <!-- 生成 DAO -->
<records>true</records> <!-- 生成 Record -->
</generate>
<target>
<packageName>com.example.generated</packageName>
<directory>src/main/java</directory>
</target>
</generator>
</configuration>
</plugin>
</plugins>
</build>
执行 mvn jooq-codegen:generate,jOOQ 连接数据库,生成以下类:
com.example.generated.tables
├── Orders.java ← 表对象
├── Users.java
└── OrderItem.java
com.example.generated.tables.records
├── OrdersRecord.java ← 行记录
├── UsersRecord.java
└── OrderItemRecord.java
com.example.generated.pojos
├── Orders.java ← POJO
├── Users.java
└── OrderItem.java
3.3 application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo
username: root
password: root123
hikari:
maximum-pool-size: 20
jooq:
sql-dialect: MYSQL # 方言
3.4 DSLContext 注入
Spring Boot 自动配置 DefaultDSLContext:
@Service
public class OrderRepository {
private final DSLContext create;
// 自动注入
public OrderRepository(DSLContext create) {
this.create = create;
}
}
四、实战 CRUD
4.1 查询
单表查询:
// SELECT * FROM orders WHERE id = ?
Orders order = create.selectFrom(ORDERS)
.where(ORDERS.ID.eq(123L))
.fetchOneInto(Orders.class);
// 多条件 + 排序 + 分页
List<Orders> list = create.selectFrom(ORDERS)
.where(ORDERS.STATUS.eq("PAID"))
.and(ORDERS.AMOUNT.greaterThan(new BigDecimal("100")))
.orderBy(ORDERS.CREATE_TIME.desc())
.limit(20)
.offset(0)
.fetchInto(Orders.class);
字段裁剪:
// SELECT id, order_no FROM orders WHERE ...
List<SimpleOrder> result = create
.select(ORDERS.ID, ORDERS.ORDER_NO)
.from(ORDERS)
.where(ORDERS.STATUS.eq("PAID"))
.fetchInto(SimpleOrder.class);
统计查询:
// SELECT COUNT(*) FROM orders WHERE status = 'PAID'
long count = create.selectCount()
.from(ORDERS)
.where(ORDERS.STATUS.eq("PAID"))
.fetchOneInto(Long.class);
// SELECT status, COUNT(*) FROM orders GROUP BY status
List<StatusCount> stats = create
.select(ORDERS.STATUS, DSL.count())
.from(ORDERS)
.groupBy(ORDERS.STATUS)
.fetchInto(StatusCount.class);
4.2 插入
// INSERT INTO orders (order_no, user_id, amount, status) VALUES (?, ?, ?, ?)
int rows = create.insertInto(ORDERS,
ORDERS.ORDER_NO, ORDERS.USER_ID, ORDERS.AMOUNT, ORDERS.STATUS)
.values("ORD123", 1L, new BigDecimal("99.9"), "PENDING")
.execute();
// 插入并返回自增主键
Long id = create.insertInto(ORDERS,
ORDERS.ORDER_NO, ORDERS.USER_ID, ORDERS.AMOUNT)
.values("ORD123", 1L, new BigDecimal("99.9"))
.returning(ORDERS.ID)
.fetchOne()
.getId();
批量插入(比 MyBatis <foreach> 简洁):
create.batchInsert(
create.newRecord(ORDERS, new Orders("ORD1", 1L, new BigDecimal("10"))),
create.newRecord(ORDERS, new Orders("ORD2", 2L, new BigDecimal("20"))),
create.newRecord(ORDERS, new Orders("ORD3", 3L, new BigDecimal("30")))
).execute();
4.3 更新
// UPDATE orders SET status = 'PAID' WHERE id = ?
int rows = create.update(ORDERS)
.set(ORDERS.STATUS, "PAID")
.set(ORDERS.PAY_TIME, LocalDateTime.now())
.where(ORDERS.ID.eq(123L))
.execute();
// UPDATE orders SET amount = amount + 100 WHERE id = ?
create.update(ORDERS)
.set(ORDERS.AMOUNT, ORDERS.AMOUNT.add(new BigDecimal("100"))) // 表达式
.where(ORDERS.ID.eq(123L))
.execute();
4.4 删除
// DELETE FROM orders WHERE id = ?
create.deleteFrom(ORDERS)
.where(ORDERS.ID.eq(123L))
.execute();
// 软删除(更常见)
create.update(ORDERS)
.set(ORDERS.DELETED, 1)
.set(ORDERS.UPDATE_TIME, LocalDateTime.now())
.where(ORDERS.ID.eq(123L))
.execute();
五、复杂联表查询
5.1 两表 JOIN
-- 要写的 SQL
SELECT o.id, o.order_no, u.username, o.amount
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 'PAID'
// jOOQ 写法
List<OrderUserVO> result = create
.select(ORDERS.ID, ORDERS.ORDER_NO, USERS.USERNAME, ORDERS.AMOUNT)
.from(ORDERS)
.join(USERS).on(ORDERS.USER_ID.eq(USERS.ID))
.where(ORDERS.STATUS.eq("PAID"))
.fetchInto(OrderUserVO.class);
5.2 三表 JOIN + 聚合
-- 复杂 SQL:每个用户的订单数和总金额
SELECT u.id, u.username,
COUNT(o.id) AS order_count,
SUM(o.amount) AS total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'PAID'
LEFT JOIN order_item oi ON o.id = oi.order_id
WHERE u.create_time >= '2026-01-01'
GROUP BY u.id, u.username
HAVING SUM(o.amount) > 1000
ORDER BY total_amount DESC
// jOOQ 写法
List<UserOrderStat> stats = create
.select(
USERS.ID,
USERS.USERNAME,
DSL.count(ORDERS.ID).as("orderCount"),
DSL.sum(ORDERS.AMOUNT).as("totalAmount")
)
.from(USERS)
.leftJoin(ORDERS).on(USERS.ID.eq(ORDERS.USER_ID))
.and(ORDERS.STATUS.eq("PAID"))
.leftJoin(ORDER_ITEM).on(ORDERS.ID.eq(ORDER_ITEM.ORDER_ID))
.where(USERS.CREATE_TIME.ge(LocalDate.of(2026, 1, 1).atStartOfDay()))
.groupBy(USERS.ID, USERS.USERNAME)
.having(DSL.sum(ORDERS.AMOUNT).gt(new BigDecimal("1000")))
.orderBy(DSL.field("totalAmount").desc())
.fetchInto(UserOrderStat.class);
对比 MyBatis 写同样 SQL:
<select id="queryUserOrderStats" resultType="UserOrderStat">
SELECT u.id, u.username,
COUNT(o.id) AS order_count,
SUM(o.amount) AS total_amount
FROM users u
LEFT JOIN orders o ON u.id = o.user_id AND o.status = 'PAID'
LEFT JOIN order_item oi ON o.id = oi.order_id
WHERE u.create_time >= #{startDate}
GROUP BY u.id, u.username
HAVING SUM(o.amount) > #{minAmount}
ORDER BY total_amount DESC
</select>
字符串拼接,改字段名时全局搜索。jOOQ 重构跟着 IDE 走。
5.3 子查询
// 查有订单的用户
List<Users> users = create
.selectFrom(USERS)
.where(USERS.ID.in(
create.selectDistinct(ORDERS.USER_ID).from(ORDERS)
))
.fetchInto(Users.class);
// EXISTS 子查询
create.selectFrom(USERS)
.whereExists(
create.selectOne().from(ORDERS)
.where(ORDERS.USER_ID.eq(USERS.ID))
)
.fetchInto(Users.class);
六、动态条件构建
6.1 MyBatis 的痛点
MyBatis 写动态条件用 <if> 标签,复杂查询时 XML 看起来像迷宫:
<select id="searchOrders" resultType="Order">
SELECT * FROM orders
<where>
<if test="status != null">
AND status = #{status}
</if>
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="minAmount != null">
AND amount >= #{minAmount}
</if>
<if test="maxAmount != null">
AND amount <= #{maxAmount}
</if>
<if test="startDate != null">
AND create_time >= #{startDate}
</if>
<if test="endDate != null">
AND create_time <= #{endDate}
</if>
</where>
ORDER BY create_time DESC
</select>
6.2 jOOQ 的 Condition 组合
public List<Orders> searchOrders(OrderQuery query) {
// 收集条件
Condition condition = DSL.noCondition(); // 1=1 等价
if (query.getStatus() != null) {
condition = condition.and(ORDERS.STATUS.eq(query.getStatus()));
}
if (query.getUserId() != null) {
condition = condition.and(ORDERS.USER_ID.eq(query.getUserId()));
}
if (query.getMinAmount() != null) {
condition = condition.and(ORDERS.AMOUNT.ge(query.getMinAmount()));
}
if (query.getMaxAmount() != null) {
condition = condition.and(ORDERS.AMOUNT.le(query.getMaxAmount()));
}
if (query.getStartDate() != null) {
condition = condition.and(ORDERS.CREATE_TIME.ge(query.getStartDate()));
}
if (query.getEndDate() != null) {
condition = condition.and(ORDERS.CREATE_TIME.le(query.getEndDate()));
}
return create.selectFrom(ORDERS)
.where(condition)
.orderBy(ORDERS.CREATE_TIME.desc())
.fetchInto(Orders.class);
}
6.3 用 DSL 条件对象组合
更优雅——构造 Condition 列表:
public List<Orders> searchOrdersV2(OrderQuery query) {
List<Condition> conditions = new ArrayList<>();
Optional.ofNullable(query.getStatus())
.map(ORDERS.STATUS::eq)
.ifPresent(conditions::add);
Optional.ofNullable(query.getUserId())
.map(ORDERS.USER_ID::eq)
.ifPresent(conditions::add);
Optional.ofNullable(query.getMinAmount())
.map(ORDERS.AMOUNT::ge)
.ifPresent(conditions::add);
Optional.ofNullable(query.getStartDate())
.map(ORDERS.CREATE_TIME::ge)
.ifPresent(conditions::add);
return create.selectFrom(ORDERS)
.where(conditions) // 自动 AND 连接
.orderBy(ORDERS.CREATE_TIME.desc())
.fetchInto(Orders.class);
}
6.4 OR 条件组合
// (status = 'PAID' AND amount > 100) OR (status = 'PENDING' AND amount > 50)
Condition c1 = ORDERS.STATUS.eq("PAID").and(ORDERS.AMOUNT.gt(100));
Condition c2 = ORDERS.STATUS.eq("PENDING").and(ORDERS.AMOUNT.gt(50));
create.selectFrom(ORDERS)
.where(c1.or(c2))
.fetch();
七、事务支持
7.1 Spring @Transactional
jOOQ 和 Spring 事务无缝集成:
@Service
public class OrderService {
@Transactional
public void createOrder(OrderRequest req) {
// 1. 创建订单
Long orderId = create.insertInto(ORDERS,
ORDERS.ORDER_NO, ORDERS.USER_ID, ORDERS.AMOUNT, ORDERS.STATUS)
.values("ORD123", req.getUserId(), req.getAmount(), "PENDING")
.returning(ORDERS.ID)
.fetchOne()
.getId();
// 2. 创建订单项
for (ItemDTO item : req.getItems()) {
create.insertInto(ORDER_ITEM,
ORDER_ITEM.ORDER_ID, ORDER_ITEM.PRODUCT_ID,
ORDER_ITEM.QUANTITY, ORDER_ITEM.PRICE)
.values(orderId, item.getProductId(),
item.getQuantity(), item.getPrice())
.execute();
}
// 3. 异常会自动回滚
if (req.getAmount().compareTo(new BigDecimal("10000")) > 0) {
throw new RuntimeException("金额超限");
}
}
}
7.2 jOOQ 原生事务
create.transaction(config -> {
DSLContext ctx = DSL.using(config);
ctx.insertInto(ORDERS, ORDERS.ORDER_NO, ORDERS.AMOUNT)
.values("ORD123", new BigDecimal("99"))
.execute();
ctx.insertInto(ORDER_ITEM, ORDER_ITEM.ORDER_ID, ORDER_ITEM.PRODUCT_ID)
.values(1L, 100L)
.execute();
});
八、常见坑
8.1 坑 1:忘记重新生成代码
问题:数据库加了字段 remark,但忘了重新生成代码。
现象:ORDERS.REMARK 编译报错"找不到符号"。
解决:每次改数据库 schema 后,立刻 mvn jooq-codegen:generate。可以在 CI 流水线里自动触发。
8.2 坑 2:生成的代码太多导致包膨胀
问题:生产库几百张表,生成的代码几十 MB。
解决:只生成需要的表:
<database>
<includes>
orders|users|order_item|product <!-- 只生成这些表 -->
</includes>
</database>
或者按 schema 拆分多个生成配置。
8.3 坑 3:Schema 变更导致编译报错淹没
问题:一个表改名,几百个文件编译报错。
解决:
- 小步重构,一次改一个表
- 用 IDE 的"批量修复"功能
- CI 检查 schema 变更影响范围
8.4 坑 4:复杂 SQL 写不出
问题:窗口函数、WITH RECURSIVE 等高级 SQL,jOOQ DSL 写起来复杂。
解决:jOOQ 支持原生 SQL 模板:
// 用 DSL.sql 写原生片段
create.select(
DSL.field("ROW_NUMBER() OVER (ORDER BY amount DESC)"),
ORDERS.ID,
ORDERS.AMOUNT
)
.from(ORDERS)
.fetch();
// 或者 DSL.sql 完整原生
create.fetch("SELECT * FROM orders WHERE id = ?", 123L);
8.5 坑 5:多数据库方言
问题:不同数据库 SQL 语法不同,jOOQ 会不会写死方言?
解决:jOOQ 支持 20+ 数据库方言,DSL 自动适配:
// 同一份代码在 MySQL 和 PostgreSQL 上跑出不同的 SQL
create.selectFrom(ORDERS).limit(10).fetch();
// MySQL: SELECT * FROM orders LIMIT 10
// Oracle: SELECT * FROM orders WHERE ROWNUM <= 10
// SQL Server: SELECT TOP 10 * FROM orders
8.6 坑 6:性能开销
问题:jOOQ 的 DSL 对象创建有开销。
解决:
// ❌ 频繁创建 Field 对象
for (Long id : ids) {
create.selectFrom(ORDERS)
.where(ORDERS.ID.eq(id))
.fetch();
}
// ✅ 批量查询
create.selectFrom(ORDERS)
.where(ORDERS.ID.in(ids))
.fetch();
// ✅ 复用静态 Field
private static final Field<Long> USER_ID = ORDERS.USER_ID;
create.selectFrom(ORDERS).where(USER_ID.eq(1L)).fetch();
8.7 坑 7:Decimal 类型精度问题
// ❌ 比较时 BigDecimal 精度问题
ORDERS.AMOUNT.eq(new BigDecimal("99.90")); // 可能不匹配 99.9
// ✅ 用 numeric 类型
ORDERS.AMOUNT.eq(DSL.val(new BigDecimal("99.90"), SQLDataType.NUMERIC(10, 2)));
8.8 坑 8:不开 SQL 日志难调试
jOOQ 生成的 SQL 默认不打印,调试不便。
配置:在 application.yml 开启:
logging:
level:
org.jooq.tools.LoggerListener: DEBUG
或代码里设置:
DSLContext create = DSL.using(connection, SQLDialect.MYSQL);
create.settings().setExecuteLogging(true);
九、JOOQ vs MyBatis:何时选哪个
9.1 选 jOOQ 的场景
- 业务 CRUD 为主(90% 的服务端开发)
- 表结构经常变(schema-first 团队)
- 重视类型安全和重构
- 团队对 Java 比 XML 熟
9.2 选 MyBatis 的场景
- SQL 复杂度高(统计/报表/复杂联表)
- DBA 团队写 SQL,开发只接 SQL
- 对 SQL 性能极致调优
- 已有大量 MyBatis 项目经验
9.3 混合使用
实际项目可以混合:
// 简单 CRUD 用 jOOQ
public Orders getOrder(Long id) {
return create.selectFrom(ORDERS)
.where(ORDERS.ID.eq(id))
.fetchOneInto(Orders.class);
}
// 复杂统计用 MyBatis(或 jOOQ 的原生 SQL)
@Select("""
SELECT DATE(create_time) AS day, COUNT(*) AS cnt
FROM orders
WHERE create_time >= #{start} AND create_time < #{end}
GROUP BY DATE(create_time)
ORDER BY day
""")
List<DailyStat> dailyStats(@Param("start") LocalDate start, @Param("end") LocalDate end);
9.4 决策树
SQL 复杂度?
├── 高(复杂联表 + 窗口函数 + 统计) → MyBatis
└── 低(标准 CRUD) → 是否需要类型安全?
├── 是 → jOOQ
└── 否 → MyBatis-Plus(更轻量)
十、总结
核心价值
| 问题 | MyBatis | jOOQ |
|---|---|---|
| 表名错 | 运行时 | 编译期 |
| 字段错 | 运行时 | 编译期 |
| 类型错 | 运行时 | 编译期 |
| 字段重命名 | 全局搜索 | 重新生成 |
| IDE 补全 | 无 | 完整 |
| 重构支持 | 弱 | 跟随 Java |
一句话
MyBatis 把 SQL 当字符串,jOOQ 把 SQL 当 Java 对象。前者运行时爆炸,后者编译期就拦住。
选型建议
- 新项目:考虑 jOOQ,类型安全和重构能力对长期维护的价值巨大
- 老项目:MyBatis 够用,逐步用 jOOQ 重构核心模块
- 统计场景:两者混合,复杂 SQL 仍用 MyBatis 或原生 SQL
何时考虑 jOOQ
- 每次改数据库 schema 都漏改 SQL 报错
- 团队对 XML SQL 维护成本高
- 想用 IDE 重构能力
- 重视长期维护性
互动话题:你们项目用 MyBatis 还是 jOOQ?有没有过线上因为 SQL 字段拼错引发的事故?欢迎留言讨论!
参考资料
标题:Java 代码里写 SQL 太痛苦了?JOOQ 类型安全查询——编译期就能发现 SQL 错误
作者:jiangyi
地址:http://www.jiangyi.space/articles/2026/08/11/1786162254095.html
公众号:服务端技术精选
- 引言
- 一、MyBatis 的痛点:SQL 是字符串
- 1.1 字符串拼接的代价
- 1.2 五类运行时才暴露的错误
- 1.3 字符串 SQL 的真实代价
- 二、JOOQ 是什么
- 2.1 核心思想
- 2.2 MyBatis vs JOOQ 对比
- 2.3 jOOQ 的另一个优势:DSL = SQL
- 三、Spring Boot 集成 jOOQ
- 3.1 添加依赖
- 3.2 代码生成插件
- 3.3 application.yml
- 3.4 DSLContext 注入
- 四、实战 CRUD
- 4.1 查询
- 4.2 插入
- 4.3 更新
- 4.4 删除
- 五、复杂联表查询
- 5.1 两表 JOIN
- 5.2 三表 JOIN + 聚合
- 5.3 子查询
- 六、动态条件构建
- 6.1 MyBatis 的痛点
- 6.2 jOOQ 的 Condition 组合
- 6.3 用 DSL 条件对象组合
- 6.4 OR 条件组合
- 七、事务支持
- 7.1 Spring @Transactional
- 7.2 jOOQ 原生事务
- 八、常见坑
- 8.1 坑 1:忘记重新生成代码
- 8.2 坑 2:生成的代码太多导致包膨胀
- 8.3 坑 3:Schema 变更导致编译报错淹没
- 8.4 坑 4:复杂 SQL 写不出
- 8.5 坑 5:多数据库方言
- 8.6 坑 6:性能开销
- 8.7 坑 7:Decimal 类型精度问题
- 8.8 坑 8:不开 SQL 日志难调试
- 九、JOOQ vs MyBatis:何时选哪个
- 9.1 选 jOOQ 的场景
- 9.2 选 MyBatis 的场景
- 9.3 混合使用
- 9.4 决策树
- 十、总结
- 核心价值
- 一句话
- 选型建议
- 何时考虑 jOOQ
- 参考资料
评论