基于 Record / POJO 增删改查
在前几篇中,我们已经完成了:
- tio-boot + jOOQ 纯配置类整合
- 事务管理
- Agroal / Druid 数据源整合
- jOOQ Codegen 强类型升级
到这里,整个基础设施已经齐备。
接下来真正进入日常开发的核心环节:
如何基于 jOOQ 生成的 Table / Record / POJO,写出清晰、强类型、可维护的增删改查代码。
本文将围绕 system_admin 表,系统讲透:
Table、Record、POJO三者分别是什么- 基于
Record的增删改查 - 基于
POJO的增删改查 Record.store()的语义saveOrUpdate的推荐实现方式batchInsert批量插入fetchOptional的更安全查询写法- 分页查询
- 条件动态拼装
- 在 tio-boot 项目中的推荐分层实践
一、先看清生成出来的三个核心对象
jOOQ Codegen 生成后,围绕一张表通常会出现三类对象。
以 system_admin 为例:
demo.jooq.gen.tables.SystemAdmindemo.jooq.gen.tables.records.SystemAdminRecorddemo.jooq.gen.tables.pojos.SystemAdmin
很多人第一次接触 jOOQ Codegen,会被这三者绕晕。
其实只要抓住一句话就够了:
Table 用来写 SQL,Record 用来表达数据库中的一行,POJO 用来承载数据。
二、表定义类:Table
生成类:
demo.jooq.gen.tables.SystemAdmin
这个类表示的是:
数据库表本身,以及它的字段定义。
例如:
public static final SystemAdmin SYSTEM_ADMIN = new SystemAdmin();
public final TableField<SystemAdminRecord, Integer> ID = ...
public final TableField<SystemAdminRecord, String> LOGIN_NAME = ...
public final TableField<SystemAdminRecord, String> PASSWORD = ...
以后写 SQL 时,就不再写字符串:
DSL.table("system_admin")
DSL.field("login_name")
而是直接写:
SYSTEM_ADMIN
SYSTEM_ADMIN.LOGIN_NAME
SYSTEM_ADMIN.PASSWORD
这就是 jOOQ Codegen 最核心的价值之一:
- 表名字段名编译期校验
- IDE 自动补全
- 重构友好
- 减少字符串 SQL 的拼写错误
三、Record:带数据库语义的一行数据
生成类:
demo.jooq.gen.tables.records.SystemAdminRecord
这个类继承自:
UpdatableRecordImpl<SystemAdminRecord>
这意味着它不是普通 Java Bean,而是:
一个和数据库表绑定的“可持久化记录对象”。
它除了能装数据,还能直接执行数据库行为,例如:
insert()update()delete()store()refresh()
例如:
SystemAdminRecord record = dsl.newRecord(SYSTEM_ADMIN);
record.setLoginName("admin");
record.setPassword("123456");
record.insert();
这不是单纯给对象赋值,而是已经可以落库。
所以 Record 的语义非常明确:
它表示表中的一行,并且这一行知道如何把自己写回数据库。
四、POJO:纯数据对象
生成类:
demo.jooq.gen.tables.pojos.SystemAdmin
POJO 是一个普通 Java 类,只有:
- 字段
- getter / setter
- equals / hashCode / toString
它不具备:
insert()update()delete()
也就是说,POJO 的定位是:
纯数据承载对象,不带数据库行为。
它更适合:
- 作为查询结果返回
- 作为 Service 层输入输出对象
- 作为 Controller 返回对象
- 作为 DAO 与上层之间的数据边界
五、为什么 jOOQ 同时生成 Record 和 POJO
很多 ORM 习惯把“实体类”做成万能对象,既承载数据,又负责持久化。
而 jOOQ 没有这样做。
它把职责拆开了:
Record 的职责
- 贴近数据库
- 带持久化行为
- 更适合 DAO 内部使用
POJO 的职责
- 只负责数据承载
- 没有副作用
- 更适合跨层传递
这种设计非常合理,因为它天然鼓励:
DAO 层处理数据库语义,上层处理业务数据。
六、DAO 基础结构
为了兼容前面文档里的事务上下文,这里继续使用统一的 useDsl() 写法。
package demo.jooq.dao;
import static demo.jooq.gen.tables.SystemAdmin.SYSTEM_ADMIN;
import org.jooq.DSLContext;
import com.litongjava.annotation.Inject;
import demo.jooq.tx.TransactionContext;
public class SystemAdminDao {
@Inject
private DSLContext dsl;
private DSLContext useDsl() {
DSLContext txDsl = TransactionContext.get();
return txDsl != null ? txDsl : dsl;
}
}
之后所有 CRUD 都基于 useDsl() 来执行。
这样可以保证:
- 非事务场景下使用全局
DSLContext - 事务场景下自动切换到当前线程事务内的
DSLContext
七、基于 Record 的 CRUD
7.1 新增:newRecord + insert
这是最典型的 Record 风格插入。
public Integer insertByRecord(String loginName, String password) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN);
record.setLoginName(loginName);
record.setPassword(password);
record.insert();
return record.getId();
}
说明
第一步:
useDsl().newRecord(SYSTEM_ADMIN)
创建一个和 system_admin 表绑定的 Record。
第二步:
record.setLoginName(...)
record.setPassword(...)
给 Record 赋值。
第三步:
record.insert();
执行插入。
如果 id 是数据库自增主键,那么插入完成后通常能拿到生成值:
record.getId()
7.2 查询单条:返回 Record
public SystemAdminRecord findRecordById(Integer id) {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOne();
}
这里的:
selectFrom(SYSTEM_ADMIN)
表示查询整张表全部字段,并且结果类型就是 SystemAdminRecord。
因为表和记录类型已经由 Codegen 绑定好了。
7.3 查询列表:返回 Record 集合
public List<SystemAdminRecord> findAllRecords() {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.orderBy(SYSTEM_ADMIN.ID.asc())
.fetch();
}
fetch() 返回的是 Result<SystemAdminRecord>,它本身可以当作列表来用。
7.4 更新:先查再改,再 update()
public boolean updatePasswordByRecord(Integer id, String newPassword) {
SystemAdminRecord record = useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOne();
if (record == null) {
return false;
}
record.setPassword(newPassword);
record.update();
return true;
}
这是典型的“对象式更新”:
- 查出 Record
- 修改字段
- 调用
update()
优点是语义直观,缺点是通常要两次数据库访问:
- 一次查询
- 一次更新
7.5 删除:先查 Record,再 delete()
public boolean deleteByRecord(Integer id) {
SystemAdminRecord record = useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOne();
if (record == null) {
return false;
}
record.delete();
return true;
}
这同样是 Record 风格的删除方式。
八、基于 POJO 的 CRUD
POJO 更适合作为查询结果和跨层数据对象。
8.1 查询单条:fetchOneInto(POJO.class)
public demo.jooq.gen.tables.pojos.SystemAdmin findPojoById(Integer id) {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOneInto(demo.jooq.gen.tables.pojos.SystemAdmin.class);
}
jOOQ 会自动把结果映射到生成的 POJO 中。
8.2 查询列表:fetchInto(POJO.class)
public List<demo.jooq.gen.tables.pojos.SystemAdmin> findAllPojos() {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.orderBy(SYSTEM_ADMIN.ID.asc())
.fetchInto(demo.jooq.gen.tables.pojos.SystemAdmin.class);
}
这个写法很常用,也很适合作为 DAO 对外返回形式。
8.3 新增:POJO 转 Record 再插入
POJO 自身不能插入数据库,需要先转换成 Record。
推荐写法:
public Integer insertByPojo(demo.jooq.gen.tables.pojos.SystemAdmin pojo) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN, pojo);
record.insert();
return record.getId();
}
这里:
newRecord(SYSTEM_ADMIN, pojo)
会根据 POJO 的字段值创建一个绑定当前 DSLContext 的 Record,比手工 new SystemAdminRecord(pojo) 更稳妥。
8.4 更新:POJO 驱动的 DSL 更新
对于 POJO 更新,生产里更推荐显式写 update 语句,而不是强依赖 Record 状态。
public int updateByPojo(demo.jooq.gen.tables.pojos.SystemAdmin pojo) {
return useDsl()
.update(SYSTEM_ADMIN)
.set(SYSTEM_ADMIN.LOGIN_NAME, pojo.getLoginName())
.set(SYSTEM_ADMIN.PASSWORD, pojo.getPassword())
.where(SYSTEM_ADMIN.ID.eq(pojo.getId()))
.execute();
}
这个写法的优点是:
- SQL 意图非常清晰
- 不依赖 Record 的状态判断
- 更适合生产环境
8.5 删除:通常直接 DSL
public int deleteById(Integer id) {
return useDsl()
.deleteFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.execute();
}
删除场景一般不需要借助 POJO。
九、Record.store() 讲透
Record.store() 是很多人刚接触 jOOQ 时最容易产生误解的地方。
它看起来很方便,因为它似乎在做“自动保存”。
但要真正用好它,必须理解它的语义。
9.1 store() 是什么
store() 的语义是:
根据当前 Record 的状态,决定执行 insert 还是 update。
也就是说,它并不是简单等于 insert(),也不简单等于 update()。
它会结合:
- Record 是否来自数据库
- 主键是否存在
- changed 状态
- 当前 Record 是否被识别为新记录
来判断执行哪种操作。
9.2 最常见的 store() 用法
新记录:执行插入
public Integer saveWithStore(String loginName, String password) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN);
record.setLoginName(loginName);
record.setPassword(password);
record.store();
return record.getId();
}
这里通常相当于执行 insert()。
已查询出的记录:执行更新
public boolean updateWithStore(Integer id, String newPassword) {
SystemAdminRecord record = useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOne();
if (record == null) {
return false;
}
record.setPassword(newPassword);
record.store();
return true;
}
这里通常相当于执行 update()。
9.3 store() 的优点
优点是代码简洁:
- 新建时可插入
- 查询后修改时可更新
对于简单单表操作,非常自然。
9.4 store() 的局限
但 store() 不是万能的,原因在于:
1. 它依赖 Record 状态
如果 Record 不是从 DSLContext 正常创建或查询出来的,而是 detached 状态,store() 的行为就可能不符合预期。
2. 它不一定适合“明确意图”的场景
有些时候,业务就是要强制 insert; 有些时候,业务就是要强制 update。
此时直接写:
record.insert();
record.update();
比 store() 更清晰。
3. 对团队可读性有要求
不是所有人一眼都能明白 store() 背后到底会执行什么 SQL。
9.5 对 store() 的建议
推荐原则:
- 简单单行保存逻辑可以使用
store() - 明确只新增时用
insert() - 明确只更新时用
update() - 对性能和意图要求高时,直接 DSL 语句更清晰
十、saveOrUpdate 的推荐实现
很多项目里都有“有主键就更新,没有主键就新增”的需求。
这类方法常被命名为:
saveOrUpdate
但这里一定要注意:
saveOrUpdate 是业务语义,不等于数据库天然提供的 UPSERT。
它至少有两种不同层面的实现方式:
- 应用层 saveOrUpdate
- 数据库层 upsert
本文先讲应用层。
10.1 方式一:基于主键判断
这是最常见、最易理解的写法。
public Integer saveOrUpdate(demo.jooq.gen.tables.pojos.SystemAdmin pojo) {
if (pojo.getId() == null) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN, pojo);
record.insert();
return record.getId();
} else {
useDsl()
.update(SYSTEM_ADMIN)
.set(SYSTEM_ADMIN.LOGIN_NAME, pojo.getLoginName())
.set(SYSTEM_ADMIN.PASSWORD, pojo.getPassword())
.where(SYSTEM_ADMIN.ID.eq(pojo.getId()))
.execute();
return pojo.getId();
}
}
适用场景
- 主键自增
- 有 id 代表更新
- 无 id 代表新增
这是最推荐作为教程默认写法的方案,因为最清晰。
10.2 方式二:先查是否存在,再决定 insert / update
如果更新依据不是主键,而是某个业务唯一键,比如 login_name,则可以这样写:
public Integer saveOrUpdateByLoginName(demo.jooq.gen.tables.pojos.SystemAdmin pojo) {
SystemAdminRecord exist = useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.LOGIN_NAME.eq(pojo.getLoginName()))
.fetchOne();
if (exist == null) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN, pojo);
record.insert();
return record.getId();
} else {
exist.setPassword(pojo.getPassword());
exist.update();
return exist.getId();
}
}
适用场景
- 业务唯一键更新
- 需要基于已存在记录做差异修改
10.3 saveOrUpdate 和数据库 UPSERT 不是一回事
应用层 saveOrUpdate 的逻辑是:
- 先判断
- 再 insert 或 update
数据库层 UPSERT 的逻辑则通常是:
- 一条 SQL 完成冲突检测和写入
在 PostgreSQL 中通常会写成:
insert into ...
on conflict (...) do update ...
在 jOOQ 中也可以表达,但这属于下一层进阶主题。
因此本文里讲的 saveOrUpdate,更准确地说是:
业务层面的 saveOrUpdate,而不是数据库级原子 upsert。
十一、fetchOptional:更安全的单条查询
很多人写单条查询时会用:
fetchOne()
这没问题,但返回值是可空的。
如果想更显式地表达“可能没有结果”,jOOQ 提供了更现代的写法:
fetchOptional()
11.1 基本用法
public Optional<SystemAdminRecord> findOptionalRecordById(Integer id) {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOptional();
}
这样返回值就是:
Optional<SystemAdminRecord>
调用方必须显式处理“有值 / 无值”两种情况。
11.2 映射成 POJO
public Optional<demo.jooq.gen.tables.pojos.SystemAdmin> findOptionalPojoById(Integer id) {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOptionalInto(demo.jooq.gen.tables.pojos.SystemAdmin.class);
}
这样非常适合 Service 层写法。
11.3 为什么推荐 fetchOptional
和 fetchOne() 比起来,它的优点是:
- 显式表达结果可能不存在
- 减少空指针风险
- 调用方处理逻辑更清晰
- 更适合现代 Java 风格
例如:
public String findPassword(Integer id) {
return findOptionalRecordById(id)
.map(SystemAdminRecord::getPassword)
.orElse(null);
}
11.4 fetchOne() 与 fetchOptional() 的区别
两者对“多条结果”的要求本质一样:
- 预期最多一条
- 如果查到多条,通常会报错
区别只是:
fetchOne()返回对象或nullfetchOptional()返回Optional<T>
所以:
fetchOptional()不是“忽略唯一性约束”,只是让“空结果”的表达更安全。
十二、分页查询
分页是后台开发最常见的需求之一。
在 jOOQ 里,最直接的分页方式就是:
limitoffset
12.1 最基础分页
public List<demo.jooq.gen.tables.pojos.SystemAdmin> paginate(int pageNo, int pageSize) {
int offset = (pageNo - 1) * pageSize;
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.orderBy(SYSTEM_ADMIN.ID.desc())
.limit(pageSize)
.offset(offset)
.fetchInto(demo.jooq.gen.tables.pojos.SystemAdmin.class);
}
说明
pageNo从 1 开始offset = (pageNo - 1) * pageSize
例如:
- 第 1 页,
offset = 0 - 第 2 页,
offset = pageSize
12.2 带总数的分页
只查当前页数据还不够,通常还要总数。
public PageResult<demo.jooq.gen.tables.pojos.SystemAdmin> paginateWithTotal(int pageNo, int pageSize) {
int offset = (pageNo - 1) * pageSize;
Integer total = useDsl()
.selectCount()
.from(SYSTEM_ADMIN)
.fetchOne(0, int.class);
List<demo.jooq.gen.tables.pojos.SystemAdmin> list = useDsl()
.selectFrom(SYSTEM_ADMIN)
.orderBy(SYSTEM_ADMIN.ID.desc())
.limit(pageSize)
.offset(offset)
.fetchInto(demo.jooq.gen.tables.pojos.SystemAdmin.class);
return new PageResult<>(pageNo, pageSize, total, list);
}
可以定义一个简单分页对象:
package demo.jooq.model;
import java.util.List;
public class PageResult<T> {
private int pageNo;
private int pageSize;
private int total;
private List<T> list;
public PageResult() {
}
public PageResult(int pageNo, int pageSize, int total, List<T> list) {
this.pageNo = pageNo;
this.pageSize = pageSize;
this.total = total;
this.list = list;
}
public int getPageNo() {
return pageNo;
}
public void setPageNo(int pageNo) {
this.pageNo = pageNo;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getTotal() {
return total;
}
public void setTotal(int total) {
this.total = total;
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
}
12.3 分页查询的注意点
1. 必须有稳定排序
分页一定要配合 orderBy,否则结果不稳定。
推荐:
.orderBy(SYSTEM_ADMIN.ID.desc())
2. 深分页要谨慎
如果页码非常深,offset 可能性能变差。
例如:
- 第 1 万页
- offset 非常大
这时更推荐“基于游标 / 上次主键”的分页方式,但这是进阶话题。
3. count 和列表查询通常分两条 SQL
这是最常见、最清晰的实现方式。
十三、条件动态拼装
这部分是 jOOQ 的强项之一。
相比 MyBatis XML 的动态标签,jOOQ 可以直接用 Java 表达式拼条件,代码更自然。
13.1 最简单的动态条件
假设有一个查询对象:
package demo.jooq.model;
public class SystemAdminQuery {
private Integer id;
private String loginName;
private String password;
private Integer pageNo;
private Integer pageSize;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getPageNo() {
return pageNo;
}
public void setPageNo(Integer pageNo) {
this.pageNo = pageNo;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
}
然后 DAO 里这样写:
import org.jooq.Condition;
import org.jooq.impl.DSL;
public List<demo.jooq.gen.tables.pojos.SystemAdmin> search(SystemAdminQuery query) {
Condition condition = DSL.trueCondition();
if (query.getId() != null) {
condition = condition.and(SYSTEM_ADMIN.ID.eq(query.getId()));
}
if (query.getLoginName() != null && !query.getLoginName().isEmpty()) {
condition = condition.and(SYSTEM_ADMIN.LOGIN_NAME.like("%" + query.getLoginName() + "%"));
}
if (query.getPassword() != null && !query.getPassword().isEmpty()) {
condition = condition.and(SYSTEM_ADMIN.PASSWORD.eq(query.getPassword()));
}
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(condition)
.orderBy(SYSTEM_ADMIN.ID.desc())
.fetchInto(demo.jooq.gen.tables.pojos.SystemAdmin.class);
}
13.2 为什么从 DSL.trueCondition() 开始
Condition condition = DSL.trueCondition();
这相当于初始化一个永远为真的条件,然后后面不断 and(...)。
好处是逻辑统一,不用写很多 if/else 判断第一个条件是谁。
13.3 动态分页查询
把动态条件和分页结合起来:
public PageResult<demo.jooq.gen.tables.pojos.SystemAdmin> searchPage(SystemAdminQuery query) {
Condition condition = DSL.trueCondition();
if (query.getId() != null) {
condition = condition.and(SYSTEM_ADMIN.ID.eq(query.getId()));
}
if (query.getLoginName() != null && !query.getLoginName().isBlank()) {
condition = condition.and(SYSTEM_ADMIN.LOGIN_NAME.like("%" + query.getLoginName() + "%"));
}
if (query.getPassword() != null && !query.getPassword().isBlank()) {
condition = condition.and(SYSTEM_ADMIN.PASSWORD.eq(query.getPassword()));
}
int pageNo = query.getPageNo() == null || query.getPageNo() < 1 ? 1 : query.getPageNo();
int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 10 : query.getPageSize();
int offset = (pageNo - 1) * pageSize;
Integer total = useDsl()
.selectCount()
.from(SYSTEM_ADMIN)
.where(condition)
.fetchOne(0, int.class);
List<demo.jooq.gen.tables.pojos.SystemAdmin> list = useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(condition)
.orderBy(SYSTEM_ADMIN.ID.desc())
.limit(pageSize)
.offset(offset)
.fetchInto(demo.jooq.gen.tables.pojos.SystemAdmin.class);
return new PageResult<>(pageNo, pageSize, total, list);
}
这就是一个非常完整的后台列表查询写法。
13.4 动态条件拼装的优点
相比 XML 动态 SQL,jOOQ 的 Java 拼装有几个明显优势:
- 全部字段都是强类型引用
- 条件逻辑天然跟随 Java 语法
- IDE 可重构
- 更容易抽公共方法
- 更适合复杂组合条件
13.5 可以进一步抽成独立条件方法
为了避免 DAO 方法越来越长,推荐把条件构造抽出来:
private Condition buildCondition(SystemAdminQuery query) {
Condition condition = DSL.trueCondition();
if (query.getId() != null) {
condition = condition.and(SYSTEM_ADMIN.ID.eq(query.getId()));
}
if (query.getLoginName() != null && !query.getLoginName().isBlank()) {
condition = condition.and(SYSTEM_ADMIN.LOGIN_NAME.like("%" + query.getLoginName() + "%"));
}
if (query.getPassword() != null && !query.getPassword().isBlank()) {
condition = condition.and(SYSTEM_ADMIN.PASSWORD.eq(query.getPassword()));
}
return condition;
}
然后查询里直接:
Condition condition = buildCondition(query);
这在项目中会更整洁。
十四、batchInsert 批量插入
批量插入是性能优化中非常常见的一步。
如果一条一条 insert():
for (...) {
record.insert();
}
虽然能用,但数据库往返次数多,性能不理想。
这时就应该考虑 jOOQ 的批量能力。
14.1 基于 Record 的批量插入
public int[] batchInsertByPojo(List<demo.jooq.gen.tables.pojos.SystemAdmin> pojos) {
List<SystemAdminRecord> records = new ArrayList<>();
for (demo.jooq.gen.tables.pojos.SystemAdmin pojo : pojos) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN, pojo);
records.add(record);
}
return useDsl().batchInsert(records).execute();
}
返回值说明
execute() 返回:
int[]
数组中的每个元素通常对应一条语句的执行结果。
14.2 使用场景
适合:
- 批量导入数据
- 初始化基础数据
- 日志类批量落库
- 一次性插入多条独立记录
14.3 批量插入的注意点
1. 批量不是无限大越好
如果一次传入 10 万条,可能:
- 占内存
- SQL 太大
- JDBC 批处理压力大
通常建议分批,例如:
- 500 条一批
- 1000 条一批
2. 批量插入更适合事务包裹
如果希望“要么全部成功,要么全部回滚”,建议在 Service 层事务中调用。
3. 自增主键回填要谨慎
不同数据库 / 驱动 / 批处理方式下,对批量插入后主键回填支持不完全一致。
所以如果非常依赖插入后逐条拿主键,需要做单独验证。
14.4 分批批量插入示例
public void batchInsertInChunks(List<demo.jooq.gen.tables.pojos.SystemAdmin> pojos, int chunkSize) {
if (pojos == null || pojos.isEmpty()) {
return;
}
int size = pojos.size();
for (int i = 0; i < size; i += chunkSize) {
int end = Math.min(i + chunkSize, size);
List<demo.jooq.gen.tables.pojos.SystemAdmin> subList = pojos.subList(i, end);
batchInsertByPojo(subList);
}
}
这种方式在导入类场景里很常见。
十五、一个完整 DAO 示例
下面给出一个把本文重点串起来的完整 DAO 示例。
package demo.jooq.dao;
import static demo.jooq.gen.tables.SystemAdmin.SYSTEM_ADMIN;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.jooq.Condition;
import org.jooq.DSLContext;
import org.jooq.impl.DSL;
import com.litongjava.annotation.Inject;
import demo.jooq.gen.tables.pojos.SystemAdmin;
import demo.jooq.gen.tables.records.SystemAdminRecord;
import demo.jooq.model.PageResult;
import demo.jooq.model.SystemAdminQuery;
import demo.jooq.tx.TransactionContext;
public class SystemAdminDao {
@Inject
private DSLContext dsl;
private DSLContext useDsl() {
DSLContext txDsl = TransactionContext.get();
return txDsl != null ? txDsl : dsl;
}
public Integer insertByRecord(String loginName, String password) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN);
record.setLoginName(loginName);
record.setPassword(password);
record.insert();
return record.getId();
}
public Integer insertByPojo(SystemAdmin pojo) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN, pojo);
record.insert();
return record.getId();
}
public Integer saveOrUpdate(SystemAdmin pojo) {
if (pojo.getId() == null) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN, pojo);
record.insert();
return record.getId();
} else {
useDsl()
.update(SYSTEM_ADMIN)
.set(SYSTEM_ADMIN.LOGIN_NAME, pojo.getLoginName())
.set(SYSTEM_ADMIN.PASSWORD, pojo.getPassword())
.where(SYSTEM_ADMIN.ID.eq(pojo.getId()))
.execute();
return pojo.getId();
}
}
public Integer saveWithStore(String loginName, String password) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN);
record.setLoginName(loginName);
record.setPassword(password);
record.store();
return record.getId();
}
public SystemAdminRecord findRecordById(Integer id) {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOne();
}
public Optional<SystemAdminRecord> findOptionalRecordById(Integer id) {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOptional();
}
public SystemAdmin findPojoById(Integer id) {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOneInto(SystemAdmin.class);
}
public Optional<SystemAdmin> findOptionalPojoById(Integer id) {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOptionalInto(SystemAdmin.class);
}
public List<SystemAdmin> findAllPojos() {
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.orderBy(SYSTEM_ADMIN.ID.asc())
.fetchInto(SystemAdmin.class);
}
public boolean updatePasswordByRecord(Integer id, String newPassword) {
SystemAdminRecord record = useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOne();
if (record == null) {
return false;
}
record.setPassword(newPassword);
record.update();
return true;
}
public int updateByPojo(SystemAdmin pojo) {
return useDsl()
.update(SYSTEM_ADMIN)
.set(SYSTEM_ADMIN.LOGIN_NAME, pojo.getLoginName())
.set(SYSTEM_ADMIN.PASSWORD, pojo.getPassword())
.where(SYSTEM_ADMIN.ID.eq(pojo.getId()))
.execute();
}
public boolean deleteByRecord(Integer id) {
SystemAdminRecord record = useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.fetchOne();
if (record == null) {
return false;
}
record.delete();
return true;
}
public int deleteById(Integer id) {
return useDsl()
.deleteFrom(SYSTEM_ADMIN)
.where(SYSTEM_ADMIN.ID.eq(id))
.execute();
}
public List<SystemAdmin> paginate(int pageNo, int pageSize) {
int offset = (pageNo - 1) * pageSize;
return useDsl()
.selectFrom(SYSTEM_ADMIN)
.orderBy(SYSTEM_ADMIN.ID.desc())
.limit(pageSize)
.offset(offset)
.fetchInto(SystemAdmin.class);
}
public PageResult<SystemAdmin> searchPage(SystemAdminQuery query) {
Condition condition = buildCondition(query);
int pageNo = query.getPageNo() == null || query.getPageNo() < 1 ? 1 : query.getPageNo();
int pageSize = query.getPageSize() == null || query.getPageSize() < 1 ? 10 : query.getPageSize();
int offset = (pageNo - 1) * pageSize;
Integer total = useDsl()
.selectCount()
.from(SYSTEM_ADMIN)
.where(condition)
.fetchOne(0, int.class);
List<SystemAdmin> list = useDsl()
.selectFrom(SYSTEM_ADMIN)
.where(condition)
.orderBy(SYSTEM_ADMIN.ID.desc())
.limit(pageSize)
.offset(offset)
.fetchInto(SystemAdmin.class);
return new PageResult<>(pageNo, pageSize, total, list);
}
public int[] batchInsertByPojo(List<SystemAdmin> pojos) {
List<SystemAdminRecord> records = new ArrayList<>();
for (SystemAdmin pojo : pojos) {
SystemAdminRecord record = useDsl().newRecord(SYSTEM_ADMIN, pojo);
records.add(record);
}
return useDsl().batchInsert(records).execute();
}
private Condition buildCondition(SystemAdminQuery query) {
Condition condition = DSL.trueCondition();
if (query.getId() != null) {
condition = condition.and(SYSTEM_ADMIN.ID.eq(query.getId()));
}
if (query.getLoginName() != null && !query.getLoginName().isBlank()) {
condition = condition.and(SYSTEM_ADMIN.LOGIN_NAME.like("%" + query.getLoginName() + "%"));
}
if (query.getPassword() != null && !query.getPassword().isBlank()) {
condition = condition.and(SYSTEM_ADMIN.PASSWORD.eq(query.getPassword()));
}
return condition;
}
}
十六、Record、POJO、DSL 三者的最终选型建议
到这里,可以把整个实践总结成一套很清晰的分工原则。
16.1 Table:只负责强类型 SQL
例如:
SYSTEM_ADMIN
SYSTEM_ADMIN.LOGIN_NAME
SYSTEM_ADMIN.PASSWORD
它是写 SQL 的基础设施。
16.2 Record:适合 DAO 内部的单行持久化操作
适合场景:
- 单条插入
- 查询后更新
- 查询后删除
- 简洁的
store()
不建议:
- 直接暴露到 Controller
- 在跨层中大量传递
- 强行承担业务 DTO 角色
16.3 POJO:适合查询结果与业务传递
适合场景:
- DAO 返回结果
- Service 层输入输出
- Controller 响应
- 批量插入输入对象
16.4 DSL 直接语句:适合复杂和高性能场景
适合场景:
- 批量更新
- 条件删除
- 分页查询
- 动态条件
- 复杂 SQL
- 对执行意图要求明确的场景
十七、几个非常容易踩的坑
17.1 不要混淆两个 SystemAdmin
你这里会同时存在两个类:
demo.jooq.gen.tables.SystemAdmindemo.jooq.gen.tables.pojos.SystemAdmin
推荐写法:
import static demo.jooq.gen.tables.SystemAdmin.SYSTEM_ADMIN;
import demo.jooq.gen.tables.pojos.SystemAdmin;
这样:
SYSTEM_ADMIN表示表SystemAdmin表示 POJO
可读性最好。
17.2 fetchOne() 和 fetchOptional() 都要求结果最多一条
它们不是“随便查第一条”。
如果条件可能返回多条,就不要用它们。
否则应该用:
fetch()
或者加上明确的唯一条件。
17.3 store() 很方便,但不要滥用
store() 依赖 Record 状态,不适合所有场景。
在团队开发里,如果想让 SQL 行为更明确:
- 新增就用
insert() - 更新就用
update()
通常比 store() 更易读。
17.4 saveOrUpdate 不是数据库原子 upsert
应用层 saveOrUpdate:
- 先判断
- 再写
数据库层 upsert:
- 一条 SQL 原子完成
两者不要混淆。
17.5 批量插入要控制批次大小
不要一次塞太多数据进去。
大批量导入时,应当:
- 分批处理
- 配合事务
- 结合数据库连接池容量和 SQL 大小压测
十八、总结
到这一篇为止,基于 jOOQ Codegen,system_admin 这张表实际上已经具备了三层开发能力:
1. SYSTEM_ADMIN
强类型表定义,解决表名字段名的编译期校验问题。
2. SystemAdminRecord
面向单行记录的对象式数据库操作,适合 DAO 内部的精细 CRUD。
3. demo.jooq.gen.tables.pojos.SystemAdmin
纯数据对象,适合作为查询结果和业务层数据承载体。
而在实际项目中,最推荐的使用方式是:
- 用
SYSTEM_ADMIN写强类型 SQL - 用
Record处理 DAO 内部单行持久化 - 用
POJO做查询结果和跨层传输 - 用 DSL 直接语句处理分页、动态条件、批量操作和复杂更新
一句话总结:
jOOQ Codegen 的真正价值,不只是“生成类”,而是把 SQL、记录对象和业务数据对象分层表达出来,让代码既强类型,又清晰可维护。
附录
SystemAdmin
/*
* This file is generated by jOOQ.
*/
package demo.jooq.gen.tables;
import demo.jooq.gen.Keys;
import demo.jooq.gen.Public;
import demo.jooq.gen.tables.records.SystemAdminRecord;
import java.util.function.Function;
import org.jooq.Field;
import org.jooq.ForeignKey;
import org.jooq.Function3;
import org.jooq.Identity;
import org.jooq.Name;
import org.jooq.Record;
import org.jooq.Records;
import org.jooq.Row3;
import org.jooq.Schema;
import org.jooq.SelectField;
import org.jooq.Table;
import org.jooq.TableField;
import org.jooq.TableOptions;
import org.jooq.UniqueKey;
import org.jooq.impl.DSL;
import org.jooq.impl.SQLDataType;
import org.jooq.impl.TableImpl;
/**
* This class is generated by jOOQ.
*/
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class SystemAdmin extends TableImpl<SystemAdminRecord> {
private static final long serialVersionUID = 1L;
/**
* The reference instance of <code>public.system_admin</code>
*/
public static final SystemAdmin SYSTEM_ADMIN = new SystemAdmin();
/**
* The class holding records for this type
*/
@Override
public Class<SystemAdminRecord> getRecordType() {
return SystemAdminRecord.class;
}
/**
* The column <code>public.system_admin.id</code>.
*/
public final TableField<SystemAdminRecord, Integer> ID = createField(DSL.name("id"), SQLDataType.INTEGER.nullable(false).identity(true), this, "");
/**
* The column <code>public.system_admin.login_name</code>.
*/
public final TableField<SystemAdminRecord, String> LOGIN_NAME = createField(DSL.name("login_name"), SQLDataType.VARCHAR(64), this, "");
/**
* The column <code>public.system_admin.password</code>.
*/
public final TableField<SystemAdminRecord, String> PASSWORD = createField(DSL.name("password"), SQLDataType.VARCHAR(64), this, "");
private SystemAdmin(Name alias, Table<SystemAdminRecord> aliased) {
this(alias, aliased, null);
}
private SystemAdmin(Name alias, Table<SystemAdminRecord> aliased, Field<?>[] parameters) {
super(alias, null, aliased, parameters, DSL.comment(""), TableOptions.table());
}
/**
* Create an aliased <code>public.system_admin</code> table reference
*/
public SystemAdmin(String alias) {
this(DSL.name(alias), SYSTEM_ADMIN);
}
/**
* Create an aliased <code>public.system_admin</code> table reference
*/
public SystemAdmin(Name alias) {
this(alias, SYSTEM_ADMIN);
}
/**
* Create a <code>public.system_admin</code> table reference
*/
public SystemAdmin() {
this(DSL.name("system_admin"), null);
}
public <O extends Record> SystemAdmin(Table<O> child, ForeignKey<O, SystemAdminRecord> key) {
super(child, key, SYSTEM_ADMIN);
}
@Override
public Schema getSchema() {
return aliased() ? null : Public.PUBLIC;
}
@Override
public Identity<SystemAdminRecord, Integer> getIdentity() {
return (Identity<SystemAdminRecord, Integer>) super.getIdentity();
}
@Override
public UniqueKey<SystemAdminRecord> getPrimaryKey() {
return Keys.SYSTEM_ADMIN_PKEY;
}
@Override
public SystemAdmin as(String alias) {
return new SystemAdmin(DSL.name(alias), this);
}
@Override
public SystemAdmin as(Name alias) {
return new SystemAdmin(alias, this);
}
@Override
public SystemAdmin as(Table<?> alias) {
return new SystemAdmin(alias.getQualifiedName(), this);
}
/**
* Rename this table
*/
@Override
public SystemAdmin rename(String name) {
return new SystemAdmin(DSL.name(name), null);
}
/**
* Rename this table
*/
@Override
public SystemAdmin rename(Name name) {
return new SystemAdmin(name, null);
}
/**
* Rename this table
*/
@Override
public SystemAdmin rename(Table<?> name) {
return new SystemAdmin(name.getQualifiedName(), null);
}
// -------------------------------------------------------------------------
// Row3 type methods
// -------------------------------------------------------------------------
@Override
public Row3<Integer, String, String> fieldsRow() {
return (Row3) super.fieldsRow();
}
/**
* Convenience mapping calling {@link SelectField#convertFrom(Function)}.
*/
public <U> SelectField<U> mapping(Function3<? super Integer, ? super String, ? super String, ? extends U> from) {
return convertFrom(Records.mapping(from));
}
/**
* Convenience mapping calling {@link SelectField#convertFrom(Class,
* Function)}.
*/
public <U> SelectField<U> mapping(Class<U> toType, Function3<? super Integer, ? super String, ? super String, ? extends U> from) {
return convertFrom(toType, Records.mapping(from));
}
}
SystemAdmin
/*
* This file is generated by jOOQ.
*/
package demo.jooq.gen.tables.pojos;
import java.io.Serializable;
/**
* This class is generated by jOOQ.
*/
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class SystemAdmin implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private String loginName;
private String password;
public SystemAdmin() {}
public SystemAdmin(SystemAdmin value) {
this.id = value.id;
this.loginName = value.loginName;
this.password = value.password;
}
public SystemAdmin(
Integer id,
String loginName,
String password
) {
this.id = id;
this.loginName = loginName;
this.password = password;
}
/**
* Getter for <code>public.system_admin.id</code>.
*/
public Integer getId() {
return this.id;
}
/**
* Setter for <code>public.system_admin.id</code>.
*/
public void setId(Integer id) {
this.id = id;
}
/**
* Getter for <code>public.system_admin.login_name</code>.
*/
public String getLoginName() {
return this.loginName;
}
/**
* Setter for <code>public.system_admin.login_name</code>.
*/
public void setLoginName(String loginName) {
this.loginName = loginName;
}
/**
* Getter for <code>public.system_admin.password</code>.
*/
public String getPassword() {
return this.password;
}
/**
* Setter for <code>public.system_admin.password</code>.
*/
public void setPassword(String password) {
this.password = password;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final SystemAdmin other = (SystemAdmin) obj;
if (this.id == null) {
if (other.id != null)
return false;
}
else if (!this.id.equals(other.id))
return false;
if (this.loginName == null) {
if (other.loginName != null)
return false;
}
else if (!this.loginName.equals(other.loginName))
return false;
if (this.password == null) {
if (other.password != null)
return false;
}
else if (!this.password.equals(other.password))
return false;
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.id == null) ? 0 : this.id.hashCode());
result = prime * result + ((this.loginName == null) ? 0 : this.loginName.hashCode());
result = prime * result + ((this.password == null) ? 0 : this.password.hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("SystemAdmin (");
sb.append(id);
sb.append(", ").append(loginName);
sb.append(", ").append(password);
sb.append(")");
return sb.toString();
}
}
SystemAdminRecord
/*
* This file is generated by jOOQ.
*/
package demo.jooq.gen.tables.records;
import demo.jooq.gen.tables.SystemAdmin;
import org.jooq.Field;
import org.jooq.Record1;
import org.jooq.Record3;
import org.jooq.Row3;
import org.jooq.impl.UpdatableRecordImpl;
/**
* This class is generated by jOOQ.
*/
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public class SystemAdminRecord extends UpdatableRecordImpl<SystemAdminRecord> implements Record3<Integer, String, String> {
private static final long serialVersionUID = 1L;
/**
* Setter for <code>public.system_admin.id</code>.
*/
public void setId(Integer value) {
set(0, value);
}
/**
* Getter for <code>public.system_admin.id</code>.
*/
public Integer getId() {
return (Integer) get(0);
}
/**
* Setter for <code>public.system_admin.login_name</code>.
*/
public void setLoginName(String value) {
set(1, value);
}
/**
* Getter for <code>public.system_admin.login_name</code>.
*/
public String getLoginName() {
return (String) get(1);
}
/**
* Setter for <code>public.system_admin.password</code>.
*/
public void setPassword(String value) {
set(2, value);
}
/**
* Getter for <code>public.system_admin.password</code>.
*/
public String getPassword() {
return (String) get(2);
}
// -------------------------------------------------------------------------
// Primary key information
// -------------------------------------------------------------------------
@Override
public Record1<Integer> key() {
return (Record1) super.key();
}
// -------------------------------------------------------------------------
// Record3 type implementation
// -------------------------------------------------------------------------
@Override
public Row3<Integer, String, String> fieldsRow() {
return (Row3) super.fieldsRow();
}
@Override
public Row3<Integer, String, String> valuesRow() {
return (Row3) super.valuesRow();
}
@Override
public Field<Integer> field1() {
return SystemAdmin.SYSTEM_ADMIN.ID;
}
@Override
public Field<String> field2() {
return SystemAdmin.SYSTEM_ADMIN.LOGIN_NAME;
}
@Override
public Field<String> field3() {
return SystemAdmin.SYSTEM_ADMIN.PASSWORD;
}
@Override
public Integer component1() {
return getId();
}
@Override
public String component2() {
return getLoginName();
}
@Override
public String component3() {
return getPassword();
}
@Override
public Integer value1() {
return getId();
}
@Override
public String value2() {
return getLoginName();
}
@Override
public String value3() {
return getPassword();
}
@Override
public SystemAdminRecord value1(Integer value) {
setId(value);
return this;
}
@Override
public SystemAdminRecord value2(String value) {
setLoginName(value);
return this;
}
@Override
public SystemAdminRecord value3(String value) {
setPassword(value);
return this;
}
@Override
public SystemAdminRecord values(Integer value1, String value2, String value3) {
value1(value1);
value2(value2);
value3(value3);
return this;
}
// -------------------------------------------------------------------------
// Constructors
// -------------------------------------------------------------------------
/**
* Create a detached SystemAdminRecord
*/
public SystemAdminRecord() {
super(SystemAdmin.SYSTEM_ADMIN);
}
/**
* Create a detached, initialised SystemAdminRecord
*/
public SystemAdminRecord(Integer id, String loginName, String password) {
super(SystemAdmin.SYSTEM_ADMIN);
setId(id);
setLoginName(loginName);
setPassword(password);
resetChangedOnNotNull();
}
/**
* Create a detached, initialised SystemAdminRecord
*/
public SystemAdminRecord(demo.jooq.gen.tables.pojos.SystemAdmin value) {
super(SystemAdmin.SYSTEM_ADMIN);
if (value != null) {
setId(value.getId());
setLoginName(value.getLoginName());
setPassword(value.getPassword());
resetChangedOnNotNull();
}
}
}
