扩展jOOQ自动生成DAO时遇只读事务INSERT执行失败问题
问题背景
我在Spring Boot应用中使用PostgreSQL数据库与jOOQ已有一段时间,使用jOOQ代码生成工具生成DAO、POJO等,当前使用spring-boot-starter-jooq 3.2.2版本(依赖jOOQ 3.18.9)。
为扩展自动生成的DAO,实现了如下代码:
@Repository public class SomeRepositoryImpl extends SomeDao { private final DSLContext dslContext; public SomeRepositoryImpl(DSLContext dslContext) { super(dslContext.configuration()); this.dslContext = dslContext; } public void save(Example example) { insert(example); } }
其中SomeDao、Example分别为自动生成的DAO与POJO,insert()方法来自jOOQ自动生成DAO的父类。
当从服务层调用someRepositoryImpl.save(example)时,抛出如下错误:
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.jooq.exception.DataAccessException: SQL [insert into "public"."example" ("name", "domain", "description", "website", "language_id") values (?, ?, ?, ?, ?) returning "public"."example"."example_id"]; ERROR: cannot execute INSERT in a read-only transaction] with root cause org.postgresql.util.PSQLException: ERROR: cannot execute INSERT in a read-only transaction at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse and so on....
我并未在代码中使用@Transactional(readOnly = true)注解,但在SomeRepositoryImpl的save方法上添加@Transactional注解后代码可正常运行;另外,直接在服务层使用自动生成的SomeDao也可正常执行插入操作:
@Autowired SomeDao someDao; someMethod(Example example){ someDao.insert(example); }
问题解答
1. 导致异常的原因
Spring Boot对jOOQ自动生成的DAO类(比如SomeDao)有内置的事务支持逻辑:这些DAO会被Spring自动创建事务代理,当调用其方法时,Spring会默认开启读写事务(除非显式指定只读)。
而你自定义的SomeRepositoryImpl虽然继承了自动生成的DAO,但Spring不会自动给它的自定义方法(比如save)创建事务代理——因为你没有给该方法添加@Transactional注解。此时调用insert方法时,底层数据库连接可能处于只读事务上下文(比如外层调用链存在未显式标注的只读事务,或者Spring默认的事务传播行为导致继承了只读属性),而jOOQ的insert操作需要读写权限,因此抛出"cannot execute INSERT in a read-only transaction"异常。
直接使用自动生成的SomeDao时,Spring的jOOQ自动配置会为其方法默认提供事务支持,所以insert操作能正常执行。
2. 扩展DAO的最佳实践
这种继承自动生成DAO的扩展方式是可行的,但并非最优解,推荐根据场景选择以下方案:
方案一:继承扩展(需注意事务注解)
如果只是简单封装通用操作(比如重命名方法),可以继续使用继承方式,但必须给自定义方法添加@Transactional注解,确保Spring为其创建读写事务代理。方案二:组合替代继承(推荐)
不要继承自动生成的DAO,而是将其注入到自定义Repository中,通过组合方式使用:@Repository public class SomeRepositoryImpl { private final SomeDao someDao; public SomeRepositoryImpl(SomeDao someDao) { this.someDao = someDao; } @Transactional public void save(Example example) { someDao.insert(example); } }这种方式更灵活,避免继承带来的事务代理问题,同时保持自动生成代码的独立性(后续重新生成DAO代码不会影响你的自定义逻辑),也便于扩展复杂业务逻辑。
方案三:直接使用DSLContext
如果需要自定义复杂SQL或批量操作,可以直接在Repository中注入DSLContext,完全自主编写数据库操作逻辑,这种方式适合高度定制化的场景。
内容的提问来源于stack exchange,提问作者Anurag Singh

