Kotlin + Exposed R2DBC:事务是否需要包裹返回Flow的查询?
嘿,我来帮你理清这个问题~
首先得先搞懂Exposed R2DBC和Flow的核心特性:Exposed的R2DBC实现里,所有实际的数据库操作都必须在事务上下文中执行,而Flow是个冷流——你在getAllStations里写的StationTable.select()只是在构建查询计划,真正去数据库拉数据、映射实体的动作,是在你调用collect的时候才触发的。
你现在的代码能跑通,完全是因为collect被包裹在dbFactory.query(也就是suspendTransaction)里面,这时候事务上下文是存在的,刚好覆盖了实际执行数据库操作的阶段。但这种写法其实有点“靠巧合运行”,不是最佳实践。
那回到你的核心问题:应该在查询方法里也包裹事务吗?我的答案是非常建议这么做,原因有两个:
职责单一&代码健壮性
数据访问层(DAO)的职责就是封装数据库操作,包括必要的事务管理。如果把事务的责任推给上层调用者,那万一有其他地方调用getAllStations时忘记包裹事务,直接就会抛出“no transaction in context”的异常,排查起来很闹心。把事务包裹在DAO方法内部,能确保任何调用这个方法的场景都能正常工作,不用依赖上层的事务上下文。修正后的
getAllStations可以改成这样:override suspend fun getAllStations(): Flow<Station> { return try { dbFactory.query { StationTable.select( StationTable.code, StationTable.name, StationTable.lat, StationTable.lng ).map { Station( code = it[StationTable.code], name = it[StationTable.name], lat = it[StationTable.lat].toFloat(), lng = it[StationTable.lng].toFloat(), ) } } } catch (_: Exception) { emptyFlow() } }事务上下文的传递性
Exposed的suspendTransaction是支持协程上下文传递的,当你在DAO方法里用dbFactory.query包裹查询后,后续collect的时候会自动继承这个事务上下文。如果上层需要更大的事务边界(比如同时调用多个DAO方法,需要保证原子性),那上层再包裹事务也没问题,Exposed会自动复用已有的事务上下文,不会新建多余的事务。
最后补充一句:你现在的写法虽然能跑,但属于“上层刚好补了事务的漏”,一旦换个调用场景就容易出问题。把事务封装在DAO内部,才是更稳妥、更符合工程规范的做法。
内容来源于stack exchange

